Xenova HF Staff commited on
Commit
64496a6
·
verified ·
1 Parent(s): b88d0d9

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,26 +18,26 @@ See the [ONNX Runtime `MatMulNBitsQkv` contrib-operator spec](https://github.com
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
 
@@ -45,13 +45,13 @@ 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
 
@@ -62,7 +62,7 @@ Attributes and default values (overridable per request):
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
@@ -71,10 +71,14 @@ Attributes and default values (overridable per request):
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
 
@@ -83,8 +87,8 @@ 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] },
@@ -93,7 +97,7 @@ const { qT, kT, vT } = await kernel({
93
  vScalesT: { data: vScalesTData, shape: [3, 1] },
94
  }, {
95
  attrs: {
96
- K: 32,
97
  Nq: 5,
98
  Nkv: 3,
99
  block_size: 32,
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `aT` | `A` | `T1` | — | — | Shared activation of rank 2 `(M, K)` or rank 3 `(batch, sequence, K)`; only the last axis is the reduction axis. | required |
24
+ | `skipT` | `skip` | `T1` | — | — | Residual added to A before the normalization, with A's shape. | optional |
25
+ | `normScaleT` | `norm_scale` | `T1` | `1` | — | Simplified-layer-normalization (RMS) gain of shape `[K]`. | required |
26
+ | `qBT` | `q_B` | `T2` | `3` | — | Bit-packed uint8 Q weights of shape `(Nq, k_blocks, blob_size)`. Bound in the packed storage layout: four blob bytes per u32 word. | required |
27
+ | `qScalesT` | `q_scales` | `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
+ | `kBT` | `k_B` | `T2` | `3` | — | Bit-packed K weights of shape `(Nkv, k_blocks, blob_size)`. Bound in the packed storage layout: four blob bytes per u32 word. | required |
29
+ | `kScalesT` | `k_scales` | `T1` | `2` | — | Per-block K scales of shape `(Nkv, k_blocks)`. | required |
30
+ | `vBT` | `v_B` | `T2` | `3` | — | Bit-packed V weights of shape `(Nkv, k_blocks, blob_size)`. Bound in the packed storage layout: four blob bytes per u32 word. | required |
31
+ | `vScalesT` | `v_scales` | `T1` | `2` | — | Per-block V scales of shape `(Nkv, k_blocks)`. | required |
32
 
33
  ## Outputs
34
 
35
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
36
  | --- | --- | --- | --- | --- | --- | --- |
37
+ | `qT` | `Q` | `T1` | same as `aT` | derived | Query projection: A's leading axes with a trailing Nq. | required |
38
+ | `kT` | `K` | `T1` | same as `aT` | derived | Key projection: A's leading axes with a trailing Nkv. | required |
39
+ | `vT` | `V` | `T1` | same as `aT` | derived | Value projection: A's leading axes with a trailing Nkv. | required |
40
+ | `residualT` | `input_skip_bias_sum` | `T1` | same as `aT` | same as `aT` | The residual sum A + skip, with A's shape. Requires the skip input. | optional |
41
 
42
  ## Attributes
43
 
 
45
 
46
  | Attribute | Default | Description |
47
  | --- | --- | --- |
 
 
 
48
  | `K` | — | Input feature dimension shared by the normalized input and all projection weights. |
 
49
  | `Nkv` | — | Output feature dimension shared by the K and V projections. |
50
+ | `Nq` | — | Output feature dimension of the Q projection. |
51
+ | `accuracy_level` | `0` | Minimum internal accuracy level, following MatMulNBits semantics; this implementation supports the standard default 0. |
52
+ | `bits` | `4` | Bit width used to quantize all three weight matrices; only 4 is supported. |
53
  | `block_size` | — | Size of each quantization block along K; only 32 is supported. |
54
+ | `epsilon` | `9.999999974752427e-7` | Epsilon used by the simplified layer-normalization reduction. |
55
 
56
  ## Type constraints
57
 
 
62
 
63
  ## Files
64
 
65
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, 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
 
71
 
72
  ## Use with `@huggingface/kernels`
73
 
74
+ ```sh
75
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
76
+ ```
77
+
78
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
79
 
80
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
81
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
82
 
83
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
84
 
 
87
 
88
  const kernel = await getKernel("webgpu-kernels/com.microsoft.MatMulNBitsQkv", { version: 1 });
89
  const { qT, kT, vT } = await kernel({
90
+ aT: { data: aTData, shape: [1, 21] },
91
+ normScaleT: { data: normScaleTData, shape: [21] },
92
  qBT: { data: qBTData, shape: [5, 1, 16] },
93
  qScalesT: { data: qScalesTData, shape: [5, 1] },
94
  kBT: { data: kBTData, shape: [3, 1, 16] },
 
97
  vScalesT: { data: vScalesTData, shape: [3, 1] },
98
  }, {
99
  attrs: {
100
+ K: 21,
101
  Nq: 5,
102
  Nkv: 3,
103
  block_size: 32,
build/webgpu/bench.json CHANGED
@@ -1,20 +1,20 @@
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": {
@@ -22,13 +22,14 @@
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 },
@@ -75,13 +76,14 @@
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 },
@@ -128,13 +130,14 @@
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 },
@@ -181,13 +184,14 @@
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 },
@@ -234,13 +238,13 @@
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 },
@@ -287,7 +291,7 @@
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
  }
 
1
  {
 
2
  "tunableSpace": { "TILE_N": [4, 8, 16], "LANES": [4, 8, 16] },
3
  "cases": [
4
  {
5
  "name": "qkv-q4-decode-k2048-nq2048-nkv512",
6
+ "tunableSpace": {},
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, "min": 0, "max": 256 },
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, "min": 0, "max": 256 },
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, "min": 0, "max": 256 },
18
  "vScalesT": { "shape": [512, 64], "dtype": "float32", "dist": "normal", "seed": 9108, "scale": 0.05 }
19
  },
20
  "outputs": {
 
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" }] }
26
  },
27
  {
28
  "name": "qkv-q4-llama-decode-k4096-nq4096-nkv4096",
29
+ "tunableSpace": {},
30
  "preset": "model",
31
  "provenance": {
32
+ "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 the packed blob: the weight ports bind in the packed storage layout (four blob bytes per u32 word), so a 4-bit code costs half a byte of traffic and the projection moves its on-disk size."
33
  },
34
  "vars": { "dtype": "float32", "tokens": 1, "K": 4096, "Nq": 4096, "Nkv": 4096 },
35
  "attrs": { "K": 4096, "Nq": 4096, "Nkv": 4096, "block_size": 32 },
 
76
  "kT": { "shape": [1, 4096], "dtype": "float32" },
77
  "vT": { "shape": [1, 4096], "dtype": "float32" }
78
  },
79
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K / 2" }] }
80
  },
81
  {
82
  "name": "qkv-q4-mistral-decode-k4096-nq4096-nkv1024",
83
+ "tunableSpace": {},
84
  "preset": "model",
85
  "provenance": {
86
+ "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 the packed blob: the weight ports bind in the packed storage layout (four blob bytes per u32 word), so a 4-bit code costs half a byte of traffic and the projection moves its on-disk size."
87
  },
88
  "vars": { "dtype": "float32", "tokens": 1, "K": 4096, "Nq": 4096, "Nkv": 1024 },
89
  "attrs": { "K": 4096, "Nq": 4096, "Nkv": 1024, "block_size": 32 },
 
130
  "kT": { "shape": [1, 1024], "dtype": "float32" },
131
  "vT": { "shape": [1, 1024], "dtype": "float32" }
132
  },
133
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K / 2" }] }
134
  },
135
  {
136
  "name": "qkv-q4-gemma3-decode-k2304-nq2048-nkv1024",
137
+ "tunableSpace": {},
138
  "preset": "model",
139
  "provenance": {
140
+ "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 the packed blob: the weight ports bind in the packed storage layout (four blob bytes per u32 word), so a 4-bit code costs half a byte of traffic and the projection moves its on-disk size."
141
  },
142
  "vars": { "dtype": "float32", "tokens": 1, "K": 2304, "Nq": 2048, "Nkv": 1024 },
143
  "attrs": { "K": 2304, "Nq": 2048, "Nkv": 1024, "block_size": 32 },
 
184
  "kT": { "shape": [1, 1024], "dtype": "float32" },
185
  "vT": { "shape": [1, 1024], "dtype": "float32" }
186
  },
187
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K / 2" }] }
188
  },
189
  {
190
  "name": "qkv-q4-phi3-decode-k3072-nq3072-nkv3072",
191
+ "tunableSpace": {},
192
  "preset": "model",
193
  "provenance": {
194
+ "notes": "Phi-3 class defaults (hidden_size 3072, num_attention_heads 32, no GQA). Bytes are the packed blob: the weight ports bind in the packed storage layout (four blob bytes per u32 word), so a 4-bit code costs half a byte of traffic and the projection moves its on-disk size."
195
  },
196
  "vars": { "dtype": "float32", "tokens": 1, "K": 3072, "Nq": 3072, "Nkv": 3072 },
197
  "attrs": { "K": 3072, "Nq": 3072, "Nkv": 3072, "block_size": 32 },
 
238
  "kT": { "shape": [1, 3072], "dtype": "float32" },
239
  "vT": { "shape": [1, 3072], "dtype": "float32" }
240
  },
241
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K / 2" }] }
242
  },
243
  {
244
  "name": "qkv-q4-llama-spec8-k4096-nq4096-nkv4096",
245
  "preset": "model",
246
  "provenance": {
247
+ "notes": "Llama class defaults with 8 rows, the shape speculative decoding verifies in one pass. Bytes are the packed blob: the weight ports bind in the packed storage layout (four blob bytes per u32 word), so a 4-bit code costs half a byte of traffic and the projection moves its on-disk size."
248
  },
249
  "vars": { "dtype": "float32", "tokens": 8, "K": 4096, "Nq": 4096, "Nkv": 4096 },
250
  "attrs": { "K": 4096, "Nq": 4096, "Nkv": 4096, "block_size": 32 },
 
291
  "kT": { "shape": [8, 4096], "dtype": "float32" },
292
  "vT": { "shape": [8, 4096], "dtype": "float32" }
293
  },
294
+ "bench": { "metrics": [{ "type": "bandwidth", "value": "(args.Nq + 2 * args.Nkv) * args.K / 2" }] }
295
  }
296
  ]
297
  }
build/webgpu/manifest.json CHANGED
@@ -2,92 +2,37 @@
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 },
@@ -98,22 +43,13 @@
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)",
@@ -131,22 +67,21 @@
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",
@@ -156,378 +91,234 @@
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
  }
 
2
  "domain": "com.microsoft",
3
  "name": "MatMulNBitsQkv",
4
  "sinceVersion": 1,
5
+ "inputs": {
6
+ "aT": { "onnx": "A", "dtype": "T1" },
7
+ "skipT": { "onnx": "skip", "dtype": "T1", "optional": true },
8
+ "normScaleT": { "onnx": "norm_scale", "dtype": "T1", "rank": 1 },
9
+ "qBT": { "onnx": "q_B", "dtype": "T2", "rank": 3, "layout": "packed" },
10
+ "qScalesT": { "onnx": "q_scales", "dtype": "T1", "rank": 2 },
11
+ "kBT": { "onnx": "k_B", "dtype": "T2", "rank": 3, "layout": "packed" },
12
+ "kScalesT": { "onnx": "k_scales", "dtype": "T1", "rank": 2 },
13
+ "vBT": { "onnx": "v_B", "dtype": "T2", "rank": 3, "layout": "packed" },
14
+ "vScalesT": { "onnx": "v_scales", "dtype": "T1", "rank": 2 }
15
+ },
16
+ "outputs": {
17
+ "qT": { "onnx": "Q", "dtype": "T1", "rank": "ranks.aT", "shape": "shapes.aT[:-1] + [attrs.Nq]" },
18
+ "kT": { "onnx": "K", "dtype": "T1", "rank": "ranks.aT", "shape": "shapes.aT[:-1] + [attrs.Nkv]" },
19
+ "vT": { "onnx": "V", "dtype": "T1", "rank": "ranks.aT", "shape": "shapes.aT[:-1] + [attrs.Nkv]" },
20
+ "residualT": {
21
+ "onnx": "input_skip_bias_sum",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  "dtype": "T1",
23
  "rank": "ranks.aT",
24
  "optional": true,
25
+ "shape": "shapes.aT"
 
26
  }
27
+ },
28
+ "attributes": {
29
+ "accuracy_level": { "default": 0 },
30
+ "bits": { "default": 4 },
31
+ "epsilon": { "default": 9.999999974752427e-7 },
32
+ "K": {},
33
+ "Nq": {},
34
+ "Nkv": {},
35
+ "block_size": {}
 
36
  },
37
  "attributeConstraints": {
38
  "K": { "required": true },
 
43
  "block_size": { "required": true, "values": [32] }
44
  },
45
  "typeConstraints": { "T1": ["float32", "float16"], "T2": ["uint8"] },
46
+ "tunables": {
47
+ "TILE_N": { "default": 8 },
48
+ "LANES": { "default": 8 },
49
+ "NORM_WORKGROUP_SIZE": { "default": 128 },
50
+ "ROW_TILE": { "default": 8 },
51
+ "DECODE_WORKGROUP_SIZE": { "default": 64 }
 
 
 
 
 
 
 
 
52
  },
 
53
  "derive": {
54
  "aRows": "numel(shapes.aT) / max(1, attrs.K)",
55
  "rowTile": "1 if aRows <= 1 else min(aRows, tunables.ROW_TILE)",
 
67
  "lanesPow2": "tunables.LANES == pow2ceil(tunables.LANES)",
68
  "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)",
69
  "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",
70
+ "decodeWalk": "aRows <= 1",
71
+ "decodeCols": "4",
72
+ "gemvWalk": "rowTile == 1 and blobSize % 16 == 0",
73
+ "decodeActVec4": "gemvWalk and attrs.K % attrs.block_size == 0",
74
+ "tileCols": "decodeCols if decodeWalk else tunables.TILE_N",
75
+ "decodeWorkgroupOk": "tunables.DECODE_WORKGROUP_SIZE >= 4 and pow2ceil(tunables.DECODE_WORKGROUP_SIZE) == tunables.DECODE_WORKGROUP_SIZE and tunables.DECODE_WORKGROUP_SIZE <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.DECODE_WORKGROUP_SIZE <= device.limits.maxComputeWorkgroupSizeX",
76
+ "projectionTiles": "ceilDiv(attrs.Nq, tileCols) + 2 * ceilDiv(attrs.Nkv, tileCols)",
77
+ "dispatchFits": "decodeWorkgroupOk and projectionTiles <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and aRows <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) 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",
78
  "aScalar": "\"f16\" if tensorDtypes.aT == \"float16\" else \"f32\"",
79
  "scalar": "\"f16\" if tensorDtypes.aT == \"float16\" else \"f32\"",
 
80
  "K": "attrs.K",
81
  "nq": "attrs.Nq",
82
  "nkv": "attrs.Nkv",
83
  "blockSize": "attrs.block_size",
 
 
84
  "bits": "attrs.bits",
 
 
85
  "defaultZero": "\"8.0\"",
86
  "tileN": "tunables.TILE_N",
87
  "lanes": "tunables.LANES",
 
91
  "hasSkip": "present.skipT",
92
  "writeResidual": "present.residualT",
93
  "K_LEN": "attrs.K",
94
+ "rowCount": "aRows",
95
+ "decodeNCols": "decodeCols",
96
+ "actVec4": "decodeActVec4",
97
+ "weightElement": "\"vec4<u32>\" if gemvWalk else \"u32\"",
98
+ "normedElement": "\"vec4<f32>\" if decodeActVec4 else \"f32\"",
99
+ "decodeWorkgroupSize": "tunables.DECODE_WORKGROUP_SIZE",
100
+ "useSubgroups": "device.features.has(\"subgroups\")"
101
  },
102
+ "when": ["dispatchFits", "qkvShapeOk"],
103
+ "bindings": {
104
+ "a": { "arg": "aT", "buffer": "read-only-storage", "elementType": "$aScalar" },
105
+ "norm_scale": { "arg": "normScaleT", "buffer": "read-only-storage", "elementType": "$aScalar", "length": "$K_LEN" },
106
+ "normed": { "scratch": "normedA", "buffer": "storage", "elementType": "f32" },
107
+ "params": { "buffer": "uniform", "struct": [{ "name": "rows", "type": "u32", "value": "aRows" }] },
108
+ "skip": { "arg": "skipT", "buffer": "read-only-storage", "elementType": "$aScalar" },
109
+ "residual": { "arg": "residualT", "buffer": "storage", "elementType": "$aScalar" },
110
+ "normed_2": {
111
+ "scratch": "normedA",
112
+ "name": "normed",
113
+ "buffer": "read-only-storage",
114
+ "elementType": "$normedElement"
115
+ },
116
+ "q_b": { "arg": "qBT", "buffer": "read-only-storage", "elementType": "$weightElement" },
117
+ "q_scales": { "arg": "qScalesT", "buffer": "read-only-storage", "elementType": "$aScalar" },
118
+ "k_b": { "arg": "kBT", "buffer": "read-only-storage", "elementType": "$weightElement" },
119
+ "k_scales": { "arg": "kScalesT", "buffer": "read-only-storage", "elementType": "$aScalar" },
120
+ "v_b": { "arg": "vBT", "buffer": "read-only-storage", "elementType": "$weightElement" },
121
+ "v_scales": { "arg": "vScalesT", "buffer": "read-only-storage", "elementType": "$aScalar" },
122
+ "q": { "arg": "qT", "buffer": "storage", "elementType": "$aScalar" },
123
+ "k": { "arg": "kT", "buffer": "storage", "elementType": "$aScalar" },
124
+ "v": { "arg": "vT", "buffer": "storage", "elementType": "$aScalar" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  },
126
  "variants": [
127
  {
128
  "id": "norm",
129
  "priority": 20,
130
+ "when": ["not present.skipT", "not present.residualT"],
131
  "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
132
  "passes": [
133
  {
134
  "id": "norm",
135
  "name": "MatMulNBitsQkv.RmsNorm",
136
  "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
137
+ "bindings": ["a", "norm_scale", "normed", "params"],
138
+ "dispatch": { "x": "min(aRows, 65535)", "y": "ceilDiv(aRows, 65535)", "z": 1 }
139
  },
140
  {
141
  "id": "main",
142
  "name": "MatMulNBitsQkv.Projection",
143
+ "shader": "qkv-projection.wgsl.jinja",
144
+ "derive": { "singleProjection": "\"\"" },
145
+ "bindings": ["normed_2", "q_b", "q_scales", "k_b", "k_scales", "v_b", "v_scales", "q", "k", "v"],
146
+ "dispatch": { "x": "projectionTiles", "y": "rowGroups" },
147
+ "subgroupCollectivesWidth": "portable"
148
  }
149
  ]
150
  },
151
  {
152
  "id": "split_norm",
153
  "priority": 10,
154
+ "when": ["not present.skipT", "not present.residualT"],
155
  "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
156
  "passes": [
157
  {
158
  "id": "norm",
159
  "name": "MatMulNBitsQkv.RmsNorm",
160
  "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
161
+ "bindings": ["a", "norm_scale", "normed", "params"],
162
+ "dispatch": { "x": "min(aRows, 65535)", "y": "ceilDiv(aRows, 65535)", "z": 1 }
163
  },
164
  {
165
  "id": "q",
166
  "name": "MatMulNBitsQkv.ProjectionQ",
167
+ "shader": "qkv-projection.wgsl.jinja",
168
+ "derive": { "singleProjection": "\"q\"" },
169
+ "bindings": ["normed_2", "q_b", "q_scales", "q"],
170
+ "dispatch": { "x": "ceilDiv(attrs.Nq, tileCols)", "y": "rowGroups" },
171
+ "subgroupCollectivesWidth": "portable"
172
  },
173
  {
174
  "id": "k",
175
  "name": "MatMulNBitsQkv.ProjectionK",
176
+ "shader": "qkv-projection.wgsl.jinja",
177
+ "derive": { "singleProjection": "\"k\"" },
178
+ "bindings": ["normed_2", "k_b", "k_scales", "k"],
179
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tileCols)", "y": "rowGroups" },
180
+ "subgroupCollectivesWidth": "portable"
181
  },
182
  {
183
  "id": "v",
184
  "name": "MatMulNBitsQkv.ProjectionV",
185
+ "shader": "qkv-projection.wgsl.jinja",
186
+ "derive": { "singleProjection": "\"v\"" },
187
+ "bindings": ["normed_2", "v_b", "v_scales", "v"],
188
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tileCols)", "y": "rowGroups" },
189
+ "subgroupCollectivesWidth": "portable"
190
  }
191
  ]
192
  },
193
  {
194
  "id": "skip",
195
  "priority": 20,
196
+ "when": ["present.skipT", "not present.residualT"],
197
  "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
198
  "passes": [
199
  {
200
  "id": "norm",
201
  "name": "MatMulNBitsQkv.RmsNorm",
202
  "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
203
+ "bindings": ["a", "skip", "norm_scale", "normed", "params"],
204
+ "dispatch": { "x": "min(aRows, 65535)", "y": "ceilDiv(aRows, 65535)", "z": 1 }
205
  },
206
  {
207
  "id": "main",
208
  "name": "MatMulNBitsQkv.Projection",
209
+ "shader": "qkv-projection.wgsl.jinja",
210
+ "derive": { "singleProjection": "\"\"" },
211
+ "bindings": ["normed_2", "q_b", "q_scales", "k_b", "k_scales", "v_b", "v_scales", "q", "k", "v"],
212
+ "dispatch": { "x": "projectionTiles", "y": "rowGroups" },
213
+ "subgroupCollectivesWidth": "portable"
214
  }
215
  ]
216
  },
217
  {
218
  "id": "split_skip",
219
  "priority": 10,
220
+ "when": ["present.skipT", "not present.residualT"],
221
  "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
222
  "passes": [
223
  {
224
  "id": "norm",
225
  "name": "MatMulNBitsQkv.RmsNorm",
226
  "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
227
+ "bindings": ["a", "skip", "norm_scale", "normed", "params"],
228
+ "dispatch": { "x": "min(aRows, 65535)", "y": "ceilDiv(aRows, 65535)", "z": 1 }
229
  },
230
  {
231
  "id": "q",
232
  "name": "MatMulNBitsQkv.ProjectionQ",
233
+ "shader": "qkv-projection.wgsl.jinja",
234
+ "derive": { "singleProjection": "\"q\"" },
235
+ "bindings": ["normed_2", "q_b", "q_scales", "q"],
236
+ "dispatch": { "x": "ceilDiv(attrs.Nq, tileCols)", "y": "rowGroups" },
237
+ "subgroupCollectivesWidth": "portable"
238
  },
239
  {
240
  "id": "k",
241
  "name": "MatMulNBitsQkv.ProjectionK",
242
+ "shader": "qkv-projection.wgsl.jinja",
243
+ "derive": { "singleProjection": "\"k\"" },
244
+ "bindings": ["normed_2", "k_b", "k_scales", "k"],
245
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tileCols)", "y": "rowGroups" },
246
+ "subgroupCollectivesWidth": "portable"
247
  },
248
  {
249
  "id": "v",
250
  "name": "MatMulNBitsQkv.ProjectionV",
251
+ "shader": "qkv-projection.wgsl.jinja",
252
+ "derive": { "singleProjection": "\"v\"" },
253
+ "bindings": ["normed_2", "v_b", "v_scales", "v"],
254
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tileCols)", "y": "rowGroups" },
255
+ "subgroupCollectivesWidth": "portable"
256
  }
257
  ]
258
  },
259
  {
260
  "id": "skipsum",
261
  "priority": 20,
262
+ "when": ["present.skipT", "present.residualT"],
263
  "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
264
  "passes": [
265
  {
266
  "id": "norm",
267
  "name": "MatMulNBitsQkv.RmsNorm",
268
  "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
269
+ "bindings": ["a", "skip", "norm_scale", "normed", "residual", "params"],
270
+ "dispatch": { "x": "min(aRows, 65535)", "y": "ceilDiv(aRows, 65535)", "z": 1 }
271
  },
272
  {
273
  "id": "main",
274
  "name": "MatMulNBitsQkv.Projection",
275
+ "shader": "qkv-projection.wgsl.jinja",
276
+ "derive": { "singleProjection": "\"\"" },
277
+ "bindings": ["normed_2", "q_b", "q_scales", "k_b", "k_scales", "v_b", "v_scales", "q", "k", "v"],
278
+ "dispatch": { "x": "projectionTiles", "y": "rowGroups" },
279
+ "subgroupCollectivesWidth": "portable"
280
  }
281
  ]
282
  },
283
  {
284
  "id": "split_skipsum",
285
  "priority": 10,
286
+ "when": ["present.skipT", "present.residualT"],
287
  "intermediates": [{ "id": "normedA", "dtype": "float32", "shape": "[numel(shapes.aT)]" }],
288
  "passes": [
289
  {
290
  "id": "norm",
291
  "name": "MatMulNBitsQkv.RmsNorm",
292
  "shader": "matmul-nbits-fused-rms-norm.wgsl.jinja",
293
+ "bindings": ["a", "skip", "norm_scale", "normed", "residual", "params"],
294
+ "dispatch": { "x": "min(aRows, 65535)", "y": "ceilDiv(aRows, 65535)", "z": 1 }
295
  },
296
  {
297
  "id": "q",
298
  "name": "MatMulNBitsQkv.ProjectionQ",
299
+ "shader": "qkv-projection.wgsl.jinja",
300
+ "derive": { "singleProjection": "\"q\"" },
301
+ "bindings": ["normed_2", "q_b", "q_scales", "q"],
302
+ "dispatch": { "x": "ceilDiv(attrs.Nq, tileCols)", "y": "rowGroups" },
303
+ "subgroupCollectivesWidth": "portable"
304
  },
305
  {
306
  "id": "k",
307
  "name": "MatMulNBitsQkv.ProjectionK",
308
+ "shader": "qkv-projection.wgsl.jinja",
309
+ "derive": { "singleProjection": "\"k\"" },
310
+ "bindings": ["normed_2", "k_b", "k_scales", "k"],
311
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tileCols)", "y": "rowGroups" },
312
+ "subgroupCollectivesWidth": "portable"
313
  },
314
  {
315
  "id": "v",
316
  "name": "MatMulNBitsQkv.ProjectionV",
317
+ "shader": "qkv-projection.wgsl.jinja",
318
+ "derive": { "singleProjection": "\"v\"" },
319
+ "bindings": ["normed_2", "v_b", "v_scales", "v"],
320
+ "dispatch": { "x": "ceilDiv(attrs.Nkv, tileCols)", "y": "rowGroups" },
321
+ "subgroupCollectivesWidth": "portable"
322
  }
323
  ]
324
  }
build/webgpu/matmul-nbits-fused-rms-norm.wgsl.jinja CHANGED
@@ -1,14 +1,10 @@
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 }};
@@ -88,11 +84,10 @@ fn row_value(index: u32) -> f32 {
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
  }
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
+ // Fused RMS-normalization pass.
4
  // normed[row, d] = (A + skip)[row, d] * inverseSqrt(mean_d((A + skip)^2) + eps) * norm_scale[d]
5
  // One workgroup owns one row. Every intermediate stays in f32 and `normed` is an
6
+ // f32 scratch tensor. The following projection therefore consumes the
7
+ // normalized values without an intervening storage-type narrowing.
 
8
  const HIDDEN: u32 = {{ hidden }}u;
9
  const WG: u32 = {{ workgroupSize }}u;
10
  const EPSILON: f32 = {{ epsilon }};
 
84
 
85
  @compute @workgroup_size(WG, 1, 1)
86
  fn main(@builtin(workgroup_id) wg: vec3<u32>,
 
87
  @builtin(local_invocation_id) lid: vec3<u32>) {
88
  // 2D-folded row index: wg.y carries the high bits past the
89
+ // per-axis dispatch fold width. Reduces to wg.x when the dispatch does not fold.
90
+ let row = wg.x + wg.y * {{ DISPATCH_FOLD_WIDTH }}u;
91
  if (row >= params.rows) {
92
  return;
93
  }
build/webgpu/metadata.json CHANGED
@@ -1,19 +1,29 @@
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
  }
 
1
  {
2
  "name": "com.microsoft.MatMulNBitsQkv",
3
+ "id": "_com_microsoft_matmulnbitsqkv_webgpu_f6dfd6b",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "58HIjRCefPnXYJoowt4ER40ZwXZBaytxqPwmLff2bUM=",
11
+ "manifest.json": "fAGWh8u4euc77NPIK/A78FwhhxsO0TXSThumgnholyE=",
12
+ "matmul-nbits-fused-rms-norm.wgsl.jinja": "4lOdB+RprQh3iv29i6RV8UWkn8S5y1aK6Te5nJpxuEk=",
13
+ "qkv-projection.wgsl.jinja": "M0SyxodhZFrp/YOpcBTegsqP0M2Vz98ZwSMxSDDa8ew=",
14
+ "test.json": "tpaBr6bhveKXElA96j2p78/ivs6jAtlZyTmSxQbbb5w="
15
  }
16
  },
17
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
18
+ "webgpu": {
19
+ "manifestSpec": "2.0",
20
+ "variants": {
21
+ "norm": ["matmul-nbits-fused-rms-norm.wgsl.jinja", "qkv-projection.wgsl.jinja"],
22
+ "split_norm": ["matmul-nbits-fused-rms-norm.wgsl.jinja", "qkv-projection.wgsl.jinja"],
23
+ "skip": ["matmul-nbits-fused-rms-norm.wgsl.jinja", "qkv-projection.wgsl.jinja"],
24
+ "split_skip": ["matmul-nbits-fused-rms-norm.wgsl.jinja", "qkv-projection.wgsl.jinja"],
25
+ "skipsum": ["matmul-nbits-fused-rms-norm.wgsl.jinja", "qkv-projection.wgsl.jinja"],
26
+ "split_skipsum": ["matmul-nbits-fused-rms-norm.wgsl.jinja", "qkv-projection.wgsl.jinja"]
27
+ }
28
+ }
29
  }
build/webgpu/qkv-projection.wgsl.jinja CHANGED
@@ -1,30 +1,28 @@
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
@@ -42,45 +40,231 @@ enable f16;
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) %}
@@ -140,14 +324,14 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
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
@@ -194,11 +378,11 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
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
  }
@@ -218,4 +402,4 @@ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid:
218
  {% endfor %}
219
  }
220
  {% endif %}
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 = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 2u);
5
+ let shift = (byte_index & 3u) * 8u + (offset & 3u) * 2u;
6
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 3u;
 
7
  {% elif bits == 4 %}
8
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 1u);
9
+ let shift = (byte_index & 3u) * 8u + (offset & 1u) * 4u;
10
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 15u;
 
11
  {% else %}
12
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
13
+ return ({{ buffer }}[byte_index >> 2u] >> ((byte_index & 3u) * 8u)) & 255u;
14
  {% endif %}
15
  }
16
  {%- endmacro %}
17
 
18
+ {% if gemvWalk and useSubgroups %}
19
+ enable subgroups;
20
  {% endif %}
21
  {{ env.wgsl.resourceDeclarations }}
22
 
23
  // com.microsoft.MatMulNBitsQkv, projection pass.
24
  // Q[row, n] = dot(A_norm[row], q_weight[n]), and likewise K and V.
25
+ {% if singleProjection %}
26
  // This specialization binds and computes one projection.
27
  {% else %}
28
  // All three projections read the same normalized row, so one dispatch covers
 
40
  // A workgroup also covers ROW_TILE activation rows, reusing each unpacked code
41
  // across their accumulators.
42
  const K: u32 = {{ K }}u;
43
+ {% if singleProjection == "q" %}
44
  const N: u32 = {{ nq }}u;
45
+ {% elif singleProjection %}
46
  const N: u32 = {{ nkv }}u;
47
  {% else %}
48
  const NQ: u32 = {{ nq }}u;
49
  const NKV: u32 = {{ nkv }}u;
50
  {% endif %}
51
+ {% if not gemvWalk %}
52
  const BLOCK_SIZE: u32 = {{ blockSize }}u;
53
+ {% endif %}
54
  const KBLOCKS: u32 = {{ kBlocks }}u;
55
  const BLOB_SIZE: u32 = {{ blobSize }}u;
56
+ {% if gemvWalk %}
57
+ const N_COLS: u32 = {{ decodeNCols }}u;
58
+ const WG: u32 = {{ decodeWorkgroupSize }}u;
59
+ const ROWS: u32 = {{ rowCount }}u;
60
+ const ZERO: f32 = {{ defaultZero }};
61
+ {% if not singleProjection %}
62
+ const Q_TILES: u32 = (NQ + N_COLS - 1u) / N_COLS;
63
+ const KV_TILES: u32 = (NKV + N_COLS - 1u) / N_COLS;
64
+ {% endif %}
65
+ {% else %}
66
  const TILE_N: u32 = {{ tileN }}u;
67
  const LANES: u32 = {{ lanes }}u;
68
  const ROW_TILE: u32 = {{ rowTile }}u;
69
+ const ROWS: u32 = {{ rowCount }}u;
70
  const WG: u32 = TILE_N * LANES;
71
  const ZERO: f32 = {{ defaultZero }};
72
+ {% if not singleProjection %}
73
  const Q_TILES: u32 = (NQ + TILE_N - 1u) / TILE_N;
74
  const KV_TILES: u32 = (NKV + TILE_N - 1u) / TILE_N;
75
  {% endif %}
76
+ {% endif %}
77
 
78
+ {% if not gemvWalk %}
79
  const BITS: u32 = {{ bits }}u;
80
+ {% endif %}
81
+ // Codes per blob byte and the mask for one code. The blob is bound in the
82
+ // packed storage layout -- four blob bytes per u32 word -- so every load
83
+ // returns 4 * CODES_PER_BYTE codes.
84
  const CODES_PER_BYTE: u32 = {{ codesPerByte }}u;
85
  const CODE_MASK: u32 = {{ codeMask }}u;
86
+ {% if gemvWalk %}
87
+ // One vec4<u32> of the packed blob is sixteen stored bytes, so it carries
88
+ // 16 * CODES_PER_BYTE codes -- and a whole quantization block is exactly
89
+ // BLOB_SIZE / 16 of them (this walk requires blobSize % 16 == 0). A vector
90
+ // therefore never straddles two blocks, so its codes all share one scale, and
91
+ // the flat vector index needs no division at all:
92
+ //
93
+ // (n * KBLOCKS + block) * (BLOB_SIZE / 16) + slot == n * VEC_GROUPS + group
94
+ const VEC_PER_BLOCK: u32 = BLOB_SIZE / 16u;
95
+ const VEC_GROUPS: u32 = KBLOCKS * VEC_PER_BLOCK;
96
+ const CODES_PER_VEC: u32 = 16u * CODES_PER_BYTE;
97
+ {% endif %}
98
 
99
+ {% for stream in (["q", "k", "v"] if not singleProjection else [singleProjection]) %}
100
+ {% if not gemvWalk %}
101
  {{ matmul_nbits_packed_code(fn=stream ~ "_code", buffer=stream ~ "_b", kBlocks="KBLOCKS", blobSize="BLOB_SIZE", bits=bits) }}
102
+ // Decode two consecutive reduction-axis codes from one stored byte. An odd
103
+ // offset would straddle bytes, so callers advance by two from an even start.
104
  fn {{ stream }}_code_pair(n: u32, block: u32, offset: u32) -> vec2<u32> {
105
+ let byte_index = (n * KBLOCKS + block) * BLOB_SIZE + offset / CODES_PER_BYTE;
106
+ let shift = (byte_index & 3u) * 8u + (offset % CODES_PER_BYTE) * BITS;
107
+ let word = {{ stream }}_b[byte_index >> 2u];
108
  return vec2<u32>((word >> shift) & CODE_MASK, (word >> (shift + BITS)) & CODE_MASK);
109
  }
110
+ {% endif %}
111
  {% endfor %}
112
 
113
+ {% if gemvWalk %}
114
+ var<workgroup> partials: array<vec4<f32>, WG>;
115
+
116
+ {% set codesPerWord = 4 * codesPerByte %}
117
+ {% macro gemv_project(weights, scalesBuffer) %}
118
+ for (var g = tid; g < VEC_GROUPS; g = g + WG) {
119
+ let block = g / VEC_PER_BLOCK;
120
+ {% if actVec4 %}
121
+ // K is a whole number of blocks here, so the group lands entirely inside the
122
+ // row and the activations come from CODES_PER_VEC / 4 aligned vector loads.
123
+ let vbase = base4 + g * (CODES_PER_VEC / 4u);
124
+ {% for v in range(codesPerWord) %}
125
+ let av{{ v }} = normed[vbase + {{ v }}u];
126
+ {% endfor %}
127
+ {% for v in range(codesPerWord) %}
128
+ {% for c in range(4) %}
129
+ let a{{ v * 4 + c }} = av{{ v }}.{{ ["x", "y", "z", "w"][c] }};
130
+ {% endfor %}
131
+ {% endfor %}
132
+ {% else %}
133
+ let k0 = g * CODES_PER_VEC;
134
+ {% for j in range(16 * codesPerByte) %}
135
+ // A trailing partial block reaches past K; those codes contribute zero.
136
+ let a{{ j }} = select(0.0, normed[base + min(k0 + {{ j }}u, K - 1u)], k0 + {{ j }}u < K);
137
+ {% endfor %}
138
+ {% endif %}
139
+ let asum = {% for j in range(16 * codesPerByte) %}{{ " + " if j > 0 else "" }}a{{ j }}{% endfor %};
140
+ {% for c in range(4) %}
141
+ {% set comp = ["x", "y", "z", "w"][c] %}
142
+ {% if c == 0 %}
143
+ {
144
+ {% else %}
145
+ if (col_base + {{ c }}u < limit) {
146
+ {% endif %}
147
+ let n = col_base + {{ c }}u;
148
+ let words = {{ weights }}[n * VEC_GROUPS + g];
149
+ let scale = f32({{ scalesBuffer }}[n * KBLOCKS + block]);
150
+ var dot = 0.0;
151
+ {% for w in range(4) %}
152
+ {% for h in range(codesPerWord) %}
153
+ dot = dot + a{{ w * codesPerWord + h }} * f32((words.{{ ["x", "y", "z", "w"][w] }} >> {{ h * bits }}u) & CODE_MASK);
154
+ {% endfor %}
155
+ {% endfor %}
156
+ acc.{{ comp }} = acc.{{ comp }} + (dot - ZERO * asum) * scale;
157
+ }
158
+ {% endfor %}
159
+ }
160
+ {%- endmacro %}
161
+
162
+ @compute @workgroup_size(WG, 1, 1)
163
+ fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
164
+ @builtin(subgroup_invocation_id) sgLane: u32, @builtin(subgroup_size) sgSize: u32{% endif %}) {
165
+ let tid = lid.x;
166
+ // Rows past the end of the batch clamp onto the last real row; the store
167
+ // guard drops them.
168
+ let row = min(wg.y, ROWS - 1u);
169
+ let base = row * K;
170
+ {% if actVec4 %}
171
+ let base4 = base / 4u;
172
+ {% endif %}
173
+ {% if singleProjection %}
174
+ let col_base = wg.x * N_COLS;
175
+ let limit = N;
176
+ {% else %}
177
+ // Tile index selects the projection, so every thread in this workgroup takes
178
+ // the same arm and the reduction below stays uniform.
179
+ let tile = wg.x;
180
+ var projection = 2u;
181
+ var local_tile = tile - Q_TILES - KV_TILES;
182
+ if (tile < Q_TILES) {
183
+ projection = 0u;
184
+ local_tile = tile;
185
+ } else if (tile < Q_TILES + KV_TILES) {
186
+ projection = 1u;
187
+ local_tile = tile - Q_TILES;
188
+ }
189
+ let col_base = local_tile * N_COLS;
190
+ let limit = select(NKV, NQ, projection == 0u);
191
+ {% endif %}
192
+ // Whole workgroups past the last column return before any barrier.
193
+ if (col_base >= limit) {
194
+ return;
195
+ }
196
+ var acc = vec4<f32>(0.0);
197
+ {% if singleProjection %}
198
+ {{ gemv_project(singleProjection ~ "_b", singleProjection ~ "_scales") }}
199
+ {% else %}
200
+ if (projection == 0u) {
201
+ {{ gemv_project("q_b", "q_scales") }}
202
+ } else if (projection == 1u) {
203
+ {{ gemv_project("k_b", "k_scales") }}
204
+ } else {
205
+ {{ gemv_project("v_b", "v_scales") }}
206
+ }
207
+ {% endif %}
208
+
209
+ {% if useSubgroups %}
210
+ // Subgroup fold: one collective, then the WG / subgroup-size per-subgroup
211
+ // partials fold once through workgroup memory. The lanes of a subgroup are
212
+ // contiguous in local_invocation_id; the projection branches above are
213
+ // workgroup-uniform, so the collective runs in uniform control flow.
214
+ let sgSum = subgroupAdd(acc);
215
+ if (sgLane == 0u) {
216
+ partials[tid / sgSize] = sgSum;
217
+ }
218
+ workgroupBarrier();
219
+
220
+ if (tid == 0u && wg.y < ROWS) {
221
+ let subgroupCount = WG / sgSize;
222
+ var total = partials[0];
223
+ for (var i = 1u; i < subgroupCount; i = i + 1u) {
224
+ total = total + partials[i];
225
+ }
226
+ {% else %}
227
+ partials[tid] = acc;
228
+ workgroupBarrier();
229
+ var stride = WG / 2u;
230
+ loop {
231
+ if (stride == 0u) {
232
+ break;
233
+ }
234
+ if (tid < stride) {
235
+ partials[tid] = partials[tid] + partials[tid + stride];
236
+ }
237
+ stride = stride / 2u;
238
+ workgroupBarrier();
239
+ }
240
+
241
+ if (tid == 0u && wg.y < ROWS) {
242
+ let total = partials[0];
243
+ {% endif %}
244
+ {% for c in range(4) %}
245
+ {% set comp = ["x", "y", "z", "w"][c] %}
246
+ {% if c == 0 %}
247
+ {
248
+ {% else %}
249
+ if (col_base + {{ c }}u < limit) {
250
+ {% endif %}
251
+ let value = {{ scalar }}(total.{{ comp }});
252
+ {% if singleProjection %}
253
+ {{ singleProjection }}[row * N + col_base + {{ c }}u] = value;
254
+ {% else %}
255
+ if (projection == 0u) {
256
+ q[row * NQ + col_base + {{ c }}u] = value;
257
+ } else if (projection == 1u) {
258
+ k[row * NKV + col_base + {{ c }}u] = value;
259
+ } else {
260
+ v[row * NKV + col_base + {{ c }}u] = value;
261
+ }
262
+ {% endif %}
263
+ }
264
+ {% endfor %}
265
+ }
266
+ }
267
+ {% else %}
268
  var<workgroup> reduction: array<f32, WG * ROW_TILE>;
269
 
270
  {% macro walk_block(codeFn, guarded) %}
 
324
  let base_{{ r }} = min(row0 + {{ r }}u, ROWS - 1u) * K;
325
  {% endfor %}
326
 
327
+ {% if singleProjection %}
328
  let n = wg.x * TILE_N + column;
329
 
330
  {% for r in range(rowTile) %}
331
  var acc_{{ r }} = 0.0;
332
  {% endfor %}
333
  if (n < N) {
334
+ {{ project(singleProjection ~ "_code", singleProjection ~ "_scales") }}
335
  }
336
  {% else %}
337
  // Tile index selects the projection, so every thread in this workgroup takes
 
378
  workgroupBarrier();
379
  }
380
 
381
+ {% if singleProjection %}
382
  if (lane == 0u && n < N) {
383
  {% for r in range(rowTile) %}
384
  if (row0 + {{ r }}u < ROWS) {
385
+ {{ singleProjection }}[(row0 + {{ r }}u) * N + n] = {{ scalar }}(reduction[{{ r }}u * WG + tid]);
386
  }
387
  {% endfor %}
388
  }
 
402
  {% endfor %}
403
  }
404
  {% endif %}
405
+ }{% endif %}
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
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
  },
@@ -495,7 +494,7 @@
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": {
@@ -658,6 +657,171 @@
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
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "norm_gqa_input_vBT": [90, 17, 203, 156, 64, 241, 112, 38, 175, 229, 83, 11, 198, 147, 52, 220, 105]
4
  },
 
494
  },
495
  {
496
  "name": "two_quant_blocks",
497
+ "provenance": { "notes": "K=64 spans two 32-element quantization blocks." },
498
  "attrs": { "K": 64, "Nq": 8, "Nkv": 4, "block_size": 32 },
499
  "inputs": {
500
  "aT": {
 
657
  "kT": { "dtype": "float16", "shape": [3, 4], "tolerance": 0.002, "relTolerance": 0.01 },
658
  "vT": { "dtype": "float16", "shape": [3, 4], "tolerance": 0.002, "relTolerance": 0.01 }
659
  }
660
+ },
661
+ {
662
+ "name": "decode_multi_trip_tails",
663
+ "provenance": {
664
+ "notes": "Single row at K past two 128-code trips of the reduction-partitioned walk, with a partial final block and column counts not aligned to the decode column group."
665
+ },
666
+ "attrs": { "K": 300, "Nq": 5, "Nkv": 3, "block_size": 32 },
667
+ "inputs": {
668
+ "aT": {
669
+ "dtype": "float32",
670
+ "shape": [1, 300],
671
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
672
+ },
673
+ "normScaleT": {
674
+ "dtype": "float32",
675
+ "shape": [300],
676
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
677
+ },
678
+ "qBT": {
679
+ "dtype": "uint8",
680
+ "shape": [5, 10, 16],
681
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
682
+ },
683
+ "qScalesT": {
684
+ "dtype": "float32",
685
+ "shape": [5, 10],
686
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
687
+ },
688
+ "kBT": {
689
+ "dtype": "uint8",
690
+ "shape": [3, 10, 16],
691
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
692
+ },
693
+ "kScalesT": {
694
+ "dtype": "float32",
695
+ "shape": [3, 10],
696
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
697
+ },
698
+ "vBT": {
699
+ "dtype": "uint8",
700
+ "shape": [3, 10, 16],
701
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
702
+ },
703
+ "vScalesT": {
704
+ "dtype": "float32",
705
+ "shape": [3, 10],
706
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
707
+ }
708
+ },
709
+ "outputs": {
710
+ "qT": { "dtype": "float32", "shape": [1, 5], "tolerance": 0.0001, "relTolerance": 0.0001 },
711
+ "kT": { "dtype": "float32", "shape": [1, 3], "tolerance": 0.0001, "relTolerance": 0.0001 },
712
+ "vT": { "dtype": "float32", "shape": [1, 3], "tolerance": 0.0001, "relTolerance": 0.0001 }
713
+ }
714
+ },
715
+ {
716
+ "name": "decode_odd_k",
717
+ "provenance": { "notes": "Single row with an odd K, so the last reduction pair has one live code." },
718
+ "attrs": { "K": 21, "Nq": 5, "Nkv": 3, "block_size": 32 },
719
+ "inputs": {
720
+ "aT": {
721
+ "dtype": "float32",
722
+ "shape": [1, 21],
723
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
724
+ },
725
+ "normScaleT": {
726
+ "dtype": "float32",
727
+ "shape": [21],
728
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
729
+ },
730
+ "qBT": {
731
+ "dtype": "uint8",
732
+ "shape": [5, 1, 16],
733
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
734
+ },
735
+ "qScalesT": {
736
+ "dtype": "float32",
737
+ "shape": [5, 1],
738
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
739
+ },
740
+ "kBT": {
741
+ "dtype": "uint8",
742
+ "shape": [3, 1, 16],
743
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
744
+ },
745
+ "kScalesT": {
746
+ "dtype": "float32",
747
+ "shape": [3, 1],
748
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
749
+ },
750
+ "vBT": {
751
+ "dtype": "uint8",
752
+ "shape": [3, 1, 16],
753
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
754
+ },
755
+ "vScalesT": {
756
+ "dtype": "float32",
757
+ "shape": [3, 1],
758
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
759
+ }
760
+ },
761
+ "outputs": {
762
+ "qT": { "dtype": "float32", "shape": [1, 5], "tolerance": 0.0001, "relTolerance": 0.0001 },
763
+ "kT": { "dtype": "float32", "shape": [1, 3], "tolerance": 0.0001, "relTolerance": 0.0001 },
764
+ "vT": { "dtype": "float32", "shape": [1, 3], "tolerance": 0.0001, "relTolerance": 0.0001 }
765
+ }
766
+ },
767
+ {
768
+ "name": "decode_f16_two_trips",
769
+ "provenance": {
770
+ "notes": "Single float16 row over two trips of the reduction-partitioned walk with the skip input."
771
+ },
772
+ "attrs": { "K": 160, "Nq": 8, "Nkv": 4, "block_size": 32 },
773
+ "inputs": {
774
+ "aT": {
775
+ "dtype": "float16",
776
+ "shape": [1, 160],
777
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.8 }
778
+ },
779
+ "normScaleT": {
780
+ "dtype": "float16",
781
+ "shape": [160],
782
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4, "offset": 1.0 }
783
+ },
784
+ "qBT": {
785
+ "dtype": "uint8",
786
+ "shape": [8, 5, 16],
787
+ "data": { "kind": "cycle", "values": [27, 180, 75, 226, 33, 150, 201, 108, 57, 246, 129, 66, 195] }
788
+ },
789
+ "qScalesT": {
790
+ "dtype": "float16",
791
+ "shape": [8, 5],
792
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
793
+ },
794
+ "kBT": {
795
+ "dtype": "uint8",
796
+ "shape": [4, 5, 16],
797
+ "data": { "kind": "cycle", "values": [211, 44, 137, 98, 165, 20, 233, 121, 78, 190, 15, 252, 87, 143, 61] }
798
+ },
799
+ "kScalesT": {
800
+ "dtype": "float16",
801
+ "shape": [4, 5],
802
+ "data": { "kind": "cycle", "values": [0.045, 0.03, 0.07, 0.05, 0.08, 0.035] }
803
+ },
804
+ "vBT": {
805
+ "dtype": "uint8",
806
+ "shape": [4, 5, 16],
807
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/norm_gqa_input_vBT" } }
808
+ },
809
+ "vScalesT": {
810
+ "dtype": "float16",
811
+ "shape": [4, 5],
812
+ "data": { "kind": "cycle", "values": [0.052, 0.038, 0.061, 0.029, 0.073] }
813
+ },
814
+ "skipT": {
815
+ "dtype": "float16",
816
+ "shape": [1, 160],
817
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.11, "scale": 0.5 }
818
+ }
819
+ },
820
+ "outputs": {
821
+ "qT": { "dtype": "float16", "shape": [1, 8], "tolerance": 0.02, "relTolerance": 0.02 },
822
+ "kT": { "dtype": "float16", "shape": [1, 4], "tolerance": 0.02, "relTolerance": 0.02 },
823
+ "vT": { "dtype": "float16", "shape": [1, 4], "tolerance": 0.02, "relTolerance": 0.02 }
824
+ }
825
  }
826
  ]
827
  }