Xenova HF Staff commited on
Commit
929af3e
·
verified ·
1 Parent(s): 19bef0f

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,19 +18,19 @@ See the [ONNX Runtime `MatMulNBits` contrib-operator spec](https://github.com/mi
18
 
19
  ## Inputs
20
 
21
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
- | `A` | `aT` | `T1` | — | — | Float input matrix, not quantized. Rank 2 has shape `(M, K)` and rank 3 has shape `(batch, sequence, K)`; only the last axis is the reduction axis and the leading axes fold into the row count, so the ordinary activation needs no surrounding Reshape. | required |
24
- | `B` | `bT` | `uint8` | `3` | — | Bit-packed uint8 weight matrix of shape `(N, k_blocks, blob_size)`, where `k_blocks = ceil(K / block_size)` and `blob_size = block_size * bits / 8`. Codes are packed low-first along K. | required |
25
- | `scales` | `scalesT` | `T1` | `2` | — | Per-block dequantization scale factors of shape `(N, k_blocks)`, with the same dtype as `A`. | required |
26
- | `zero_points` | `zeroPointsT` | `T3` | `2` | — | Standard unpacked per-block zero points with shape `(N, k_blocks)` and the same dtype as `A`. Omission uses `2^(bits - 1)`. | optional |
27
- | `bias` | `biasT` | `T1` | `1` | — | Optional bias vector of shape `[N]` added to the output. | optional |
28
 
29
  ## Outputs
30
 
31
- | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
32
  | --- | --- | --- | --- | --- | --- | --- |
33
- | `Y` | `yT` | `T1` | same as `A` | derived; see description | Result of A multiplied by the dequantized weight matrix, with optional bias, same dtype and rank as A: the leading axes of A with a trailing N. | required |
34
 
35
  ## Attributes
36
 
@@ -38,10 +38,10 @@ Attributes and default values (overridable per request):
38
 
39
  | Attribute | Default | Description |
40
  | --- | --- | --- |
41
- | `accuracy_level` | `0` | Minimum internal accuracy level: 0 (unset), 1 (float32), 2 (float16), 3 (bfloat16), or 4 (int8). |
42
- | `bits` | `4` | Bit width used to quantize B; this package supports 2, 4, and 8. |
43
  | `K` | — | Input feature dimension of the weight matrix. |
44
  | `N` | — | Output feature dimension of the weight matrix. |
 
 
45
  | `block_size` | — | Power-of-two quantization block size along K; it must be at least 16. |
46
 
47
  ## Type constraints
@@ -51,13 +51,26 @@ Attributes and default values (overridable per request):
51
  | `T1` | `float32`, `float16` |
52
  | `T3` | `float32`, `float16` |
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  ## Device requirements
55
 
56
  Some implementation variants require `subgroup-matrix` and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
57
 
58
  ## Files
59
 
60
- - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
61
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
62
  - [`test.json`](build/webgpu/test.json) — correctness cases
63
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
@@ -69,13 +82,18 @@ Some implementation variants require `subgroup-matrix` and `subgroups`. These ar
69
  - [`matmul-nbits-q4-prefill-tiled.wgsl.jinja`](build/webgpu/matmul-nbits-q4-prefill-tiled.wgsl.jinja)
70
  - [`matmul-nbits-q4-sgmat.wgsl.jinja`](build/webgpu/matmul-nbits-q4-sgmat.wgsl.jinja)
71
  - [`matmul-nbits.wgsl.jinja`](build/webgpu/matmul-nbits.wgsl.jinja)
 
72
 
73
  ## Use with `@huggingface/kernels`
74
 
75
- The loader derives every required output's shape and logical dtype from the manifest contract and this call.
76
- It then allocates the result tensors automatically.
 
 
 
77
 
78
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
 
79
 
80
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
81
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
22
  | --- | --- | --- | --- | --- | --- | --- |
23
+ | `aT` | `A` | `T1` | — | — | Float input matrix, not quantized. Rank 2 has shape `(M, K)` and rank 3 has shape `(batch, sequence, K)`; only the last axis is the reduction axis and the leading axes fold into the row count, so the ordinary activation needs no surrounding Reshape. | required |
24
+ | `bT` | `B` | `uint8` | `3` | — | Bit-packed uint8 weight matrix of shape `(N, k_blocks, blob_size)`, where `k_blocks = ceil(K / block_size)` and `blob_size = block_size * bits / 8`. Codes are packed low-first along K. Bound in the packed storage layout: four blob bytes per u32 word, so the kernels stream the blob's own bytes rather than one widened word per byte. | required |
25
+ | `scalesT` | `scales` | `T1` | `2` | — | Per-block dequantization scale factors of shape `(N, k_blocks)`, with the same dtype as `A`. | required |
26
+ | `zeroPointsT` | `zero_points` | `T3` | `2` | — | Standard unpacked per-block zero points with shape `(N, k_blocks)` and the same dtype as `A`. Omission uses `2^(bits - 1)`. | optional |
27
+ | `biasT` | `bias` | `T1` | `1` | — | Optional bias vector of shape `[N]` added to the output. | optional |
28
 
29
  ## Outputs
30
 
31
+ | Name | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
32
  | --- | --- | --- | --- | --- | --- | --- |
33
+ | `yT` | `Y` | `T1` | same as `aT` | derived | Result of A multiplied by the dequantized weight matrix, with optional bias, same dtype and rank as A: the leading axes of A with a trailing N. | required |
34
 
35
  ## Attributes
36
 
 
38
 
39
  | Attribute | Default | Description |
40
  | --- | --- | --- |
 
 
41
  | `K` | — | Input feature dimension of the weight matrix. |
42
  | `N` | — | Output feature dimension of the weight matrix. |
43
+ | `accuracy_level` | `0` | Minimum internal accuracy level: 0 (unset), 1 (float32), 2 (float16), 3 (bfloat16), or 4 (int8). |
44
+ | `bits` | `4` | Bit width used to quantize B; this package supports 2, 4, and 8. |
45
  | `block_size` | — | Power-of-two quantization block size along K; it must be at least 16. |
46
 
47
  ## Type constraints
 
51
  | `T1` | `float32`, `float16` |
52
  | `T3` | `float32`, `float16` |
53
 
54
+ ## Implementation variants
55
+
56
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
57
+
58
+ - `prefill_tiled_reg_vec4_splitk_default_zero` — Cuts the K reduction of the four-wide register-blocked prefill tile into power-of-two slices across dispatch.z, retaining at least 512 K values per slice, then sums the f32 partials and adds bias in a second pass.
59
+ - `prefill_tiled_reg_vec4_default_zero` — The register-blocked prefill tile with four-wide activation loads and a 128-row tile above 256 rows: the activation slice of every K tile is staged with one vector load per lane instead of four scalar loads, and the taller tile halves the dequantization work per multiply-add.
60
+ - `prefill_tiled_reg_vec4_splitk_zero_bias` — Cuts the K reduction of the four-wide register-blocked prefill tile into power-of-two slices across dispatch.z, retaining at least 512 K values per slice, then sums the f32 partials and adds bias in a second pass.
61
+ - `prefill_tiled_reg_vec4_zero_bias` — The register-blocked prefill tile with four-wide activation loads and a 128-row tile above 256 rows: the activation slice of every K tile is staged with one vector load per lane instead of four scalar loads, and the taller tile halves the dequantization work per multiply-add.
62
+ - `prefill_tiled_reg_vec4_splitk_zero_only` — Cuts the K reduction of the four-wide register-blocked prefill tile into power-of-two slices across dispatch.z, retaining at least 512 K values per slice, then sums the f32 partials and adds bias in a second pass.
63
+ - `prefill_tiled_reg_vec4_zero_only` — The register-blocked prefill tile with four-wide activation loads and a 128-row tile above 256 rows: the activation slice of every K tile is staged with one vector load per lane instead of four scalar loads, and the taller tile halves the dequantization work per multiply-add.
64
+ - `prefill_tiled_reg_vec4_splitk_bias_only` — Cuts the K reduction of the four-wide register-blocked prefill tile into power-of-two slices across dispatch.z, retaining at least 512 K values per slice, then sums the f32 partials and adds bias in a second pass.
65
+ - `prefill_tiled_reg_vec4_bias_only` — The register-blocked prefill tile with four-wide activation loads and a 128-row tile above 256 rows: the activation slice of every K tile is staged with one vector load per lane instead of four scalar loads, and the taller tile halves the dequantization work per multiply-add.
66
+
67
  ## Device requirements
68
 
69
  Some implementation variants require `subgroup-matrix` and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
70
 
71
  ## Files
72
 
73
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
74
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
75
  - [`test.json`](build/webgpu/test.json) — correctness cases
76
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
82
  - [`matmul-nbits-q4-prefill-tiled.wgsl.jinja`](build/webgpu/matmul-nbits-q4-prefill-tiled.wgsl.jinja)
83
  - [`matmul-nbits-q4-sgmat.wgsl.jinja`](build/webgpu/matmul-nbits-q4-sgmat.wgsl.jinja)
84
  - [`matmul-nbits.wgsl.jinja`](build/webgpu/matmul-nbits.wgsl.jinja)
85
+ - [`reduce-axis0-splitk-combine.wgsl.jinja`](build/webgpu/reduce-axis0-splitk-combine.wgsl.jinja)
86
 
87
  ## Use with `@huggingface/kernels`
88
 
89
+ ```sh
90
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
91
+ ```
92
+
93
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
94
 
95
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
96
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
97
 
98
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
99
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.MatMulNBits",
3
  "tunableSpace": { "workgroupSize": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -33,7 +32,7 @@
33
  "metrics": [
34
  {
35
  "type": "bandwidth",
36
- "value": "numel(shapes.bT) * 4 + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.biasT) + numel(shapes.yT)) * 4"
37
  }
38
  ]
39
  },
@@ -71,7 +70,7 @@
71
  "metrics": [
72
  {
73
  "type": "bandwidth",
74
- "value": "numel(shapes.bT) * 4 + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.biasT) + numel(shapes.yT)) * 4"
75
  }
76
  ]
77
  },
@@ -143,7 +142,7 @@
143
  "metrics": [
144
  {
145
  "type": "bandwidth",
146
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
147
  }
148
  ]
149
  },
@@ -215,7 +214,7 @@
215
  "metrics": [
216
  {
217
  "type": "bandwidth",
218
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
219
  }
220
  ]
221
  },
@@ -243,7 +242,7 @@
243
  "metrics": [
244
  {
245
  "type": "bandwidth",
246
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
247
  }
248
  ]
249
  },
@@ -336,7 +335,7 @@
336
  "metrics": [
337
  {
338
  "type": "bandwidth",
339
- "value": "numel(shapes.bT) * 4 + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.biasT) + numel(shapes.yT)) * 4"
340
  }
341
  ]
342
  },
@@ -471,7 +470,7 @@
471
  "metrics": [
472
  {
473
  "type": "bandwidth",
474
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 2"
475
  }
476
  ]
477
  },
@@ -500,7 +499,7 @@
500
  "metrics": [
501
  {
502
  "type": "bandwidth",
503
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 2"
504
  }
505
  ]
506
  },
@@ -664,7 +663,7 @@
664
  "metrics": [
665
  {
666
  "type": "bandwidth",
667
- "value": "numel(shapes.bT) * 4 + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
668
  }
669
  ]
670
  },
@@ -756,7 +755,7 @@
756
  "metrics": [
757
  {
758
  "type": "bandwidth",
759
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
760
  }
761
  ]
762
  },
@@ -785,7 +784,7 @@
785
  "metrics": [
786
  {
787
  "type": "bandwidth",
788
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
789
  }
790
  ]
791
  },
@@ -813,11 +812,53 @@
813
  "metrics": [
814
  {
815
  "type": "bandwidth",
816
- "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
817
  }
818
  ]
819
  },
820
  "attrs": { "K": 128, "N": 262144, "bits": 4, "block_size": 32 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
  }
822
  ]
823
  }
 
1
  {
 
2
  "tunableSpace": { "workgroupSize": [64, 128, 256] },
3
  "cases": [
4
  {
 
32
  "metrics": [
33
  {
34
  "type": "bandwidth",
35
+ "value": "numel(shapes.bT) + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.biasT) + numel(shapes.yT)) * 4"
36
  }
37
  ]
38
  },
 
70
  "metrics": [
71
  {
72
  "type": "bandwidth",
73
+ "value": "numel(shapes.bT) + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.biasT) + numel(shapes.yT)) * 4"
74
  }
75
  ]
76
  },
 
142
  "metrics": [
143
  {
144
  "type": "bandwidth",
145
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
146
  }
147
  ]
148
  },
 
214
  "metrics": [
215
  {
216
  "type": "bandwidth",
217
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
218
  }
219
  ]
220
  },
 
242
  "metrics": [
243
  {
244
  "type": "bandwidth",
245
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
246
  }
247
  ]
248
  },
 
335
  "metrics": [
336
  {
337
  "type": "bandwidth",
338
+ "value": "numel(shapes.bT) + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.biasT) + numel(shapes.yT)) * 4"
339
  }
340
  ]
341
  },
 
470
  "metrics": [
471
  {
472
  "type": "bandwidth",
473
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 2"
474
  }
475
  ]
476
  },
 
499
  "metrics": [
500
  {
501
  "type": "bandwidth",
502
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 2"
503
  }
504
  ]
505
  },
 
663
  "metrics": [
664
  {
665
  "type": "bandwidth",
666
+ "value": "numel(shapes.bT) + numel(shapes.zeroPointsT) * 4 + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
667
  }
668
  ]
669
  },
 
755
  "metrics": [
756
  {
757
  "type": "bandwidth",
758
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
759
  }
760
  ]
761
  },
 
784
  "metrics": [
785
  {
786
  "type": "bandwidth",
787
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
788
  }
789
  ]
790
  },
 
812
  "metrics": [
813
  {
814
  "type": "bandwidth",
815
+ "value": "numel(shapes.bT) + (numel(shapes.aT) + numel(shapes.scalesT) + numel(shapes.yT)) * 4"
816
  }
817
  ]
818
  },
819
  "attrs": { "K": 128, "N": 262144, "bits": 4, "block_size": 32 }
820
+ },
821
+ {
822
+ "name": "mmnb-q4g32-m47-k4096-n4096-one-below-register-tile-floor-pathology",
823
+ "preset": "model",
824
+ "vars": { "M": 47, "K": 4096, "N": 4096, "bits": 4, "blockSize": 32 },
825
+ "inputs": {
826
+ "aT": { "shape": [47, 4096], "dtype": "float32", "dist": "normal", "seed": 643, "scale": 0.2 },
827
+ "bT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "q4pair", "seed": 644 },
828
+ "scalesT": {
829
+ "shape": [4096, 128],
830
+ "dtype": "float32",
831
+ "dist": "uniform",
832
+ "seed": 645,
833
+ "offset": 0.04,
834
+ "scale": 0.01,
835
+ "signed": false
836
+ }
837
+ },
838
+ "outputs": { "yT": { "shape": [47, 4096], "dtype": "float32" } },
839
+ "bench": { "metrics": [{ "type": "gflops", "value": "2 * args.M * args.K * args.N" }] },
840
+ "attrs": { "K": 4096, "N": 4096, "bits": 4, "block_size": 32 }
841
+ },
842
+ {
843
+ "name": "mmnb-q4g32-m48-k4096-n4096-register-tile-floor-control",
844
+ "preset": "model",
845
+ "vars": { "M": 48, "K": 4096, "N": 4096, "bits": 4, "blockSize": 32 },
846
+ "inputs": {
847
+ "aT": { "shape": [48, 4096], "dtype": "float32", "dist": "normal", "seed": 646, "scale": 0.2 },
848
+ "bT": { "shape": [4096, 128, 16], "dtype": "uint8", "dist": "q4pair", "seed": 647 },
849
+ "scalesT": {
850
+ "shape": [4096, 128],
851
+ "dtype": "float32",
852
+ "dist": "uniform",
853
+ "seed": 648,
854
+ "offset": 0.04,
855
+ "scale": 0.01,
856
+ "signed": false
857
+ }
858
+ },
859
+ "outputs": { "yT": { "shape": [48, 4096], "dtype": "float32" } },
860
+ "bench": { "metrics": [{ "type": "gflops", "value": "2 * args.M * args.K * args.N" }] },
861
+ "attrs": { "K": 4096, "N": 4096, "bits": 4, "block_size": 32 }
862
  }
863
  ]
864
  }
build/webgpu/manifest.json CHANGED
The diff for this file is too large to render. See raw diff
 
build/webgpu/matmul-nbits-dp4a-quantize.wgsl.jinja CHANGED
@@ -13,11 +13,10 @@ var<workgroup> maxAbs: array<f32, WG>;
13
  @compute @workgroup_size(WG)
14
  fn main(
15
  @builtin(workgroup_id) wid: vec3<u32>,
16
- @builtin(num_workgroups) nwg: vec3<u32>,
17
  @builtin(local_invocation_id) lid: vec3<u32>
18
  ) {
19
  // 2D-folded dispatch: wid.y carries the high bits past the per-dimension limit.
20
- let wgFlat = wid.x + wid.y * nwg.x;
21
  let i = wgFlat * WG + lid.x;
22
  var v = vec4<f32>(0.0);
23
  if (i < VEC4_COUNT) {
 
13
  @compute @workgroup_size(WG)
14
  fn main(
15
  @builtin(workgroup_id) wid: vec3<u32>,
 
16
  @builtin(local_invocation_id) lid: vec3<u32>
17
  ) {
18
  // 2D-folded dispatch: wid.y carries the high bits past the per-dimension limit.
19
+ let wgFlat = wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u;
20
  let i = wgFlat * WG + lid.x;
21
  var v = vec4<f32>(0.0);
22
  if (i < VEC4_COUNT) {
build/webgpu/matmul-nbits-gemv-q4.wgsl.jinja CHANGED
@@ -1,35 +1,43 @@
1
- {% if usesF16 %}
2
- enable f16;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
- // Each workgroup handles gemvNCols columns in groups of four, with one vec4
6
- // accumulator and partial array per group. Every column after the first is
7
- // bounds-guarded, so arbitrary N and group counts compose.
 
 
 
 
 
 
 
 
 
 
 
8
  {% set nCols = gemvNCols %}
9
  {% set groups = 2 if nCols == 8 else 1 %}
10
  {% set comps = ["x", "y", "z", "w"] %}
11
- {% set codesPerByte = 4 if bits == 2 else (2 if bits == 4 else 1) %}
12
- {% set codeMask = 3 if bits == 2 else (15 if bits == 4 else 255) %}
13
 
14
  const WG: u32 = {{ workgroupSize }}u;
15
  const N_COLS: u32 = {{ nCols }}u;
16
- const CODES: u32 = {{ codesPerByte }}u;
 
 
 
 
 
 
17
 
18
  {% for g in range(groups) %}
19
  {% set sfx = "" if groups == 1 else g %}
20
  var<workgroup> partials{{ sfx }}: array<vec4<f32>, {{ workgroupSize }}>;
21
  {% endfor %}
22
 
23
- // {{ bits }}-bit unpack: q2 packs four codes per byte, q4 two nibbles, q8 one byte/weight.
24
- // The whole stored byte, addressed by its index within the block rather than by
25
- // one of the codes inside it.
26
- fn packed_byte(n: u32, block: u32, byte_index: u32) -> u32 {
27
- return b[(n * params.kBlocks + block) * params.blobSize + byte_index];
28
- }
29
-
30
  // Per-(column, block) zero point. Symmetric weights use the baked default
31
- // ({{ defaultZero }}); asymmetric weights carry one widened u32 per (col, block)
32
- // in row-major [N, kBlocks] layout.
33
  fn zero_point({% if hasZero %}n: u32, block: u32{% endif %}) -> f32 {
34
  {% if hasZero %}
35
  return f32(zero_points[n * params.kBlocks + block]);
@@ -38,17 +46,27 @@ fn zero_point({% if hasZero %}n: u32, block: u32{% endif %}) -> f32 {
38
  {% endif %}
39
  }
40
 
 
 
 
 
 
 
 
 
 
41
  @compute @workgroup_size({{ workgroupSize }}, 1, 1)
42
  fn main(
43
  @builtin(workgroup_id) wid: vec3<u32>,
44
- @builtin(num_workgroups) nwg: vec3<u32>,
45
- @builtin(local_invocation_id) lid: vec3<u32>
 
46
  ) {
47
  let tid = lid.x;
48
  // 2D-folded dispatch: wid.y carries the high bits past the per-dimension
49
  // limit (vocab-sized N). The fold over-dispatches, so the tail workgroups
50
  // return before any barrier.
51
- let col_base = (wid.x + wid.y * nwg.x) * N_COLS;
52
  if (col_base >= params.N) {
53
  return;
54
  }
@@ -57,47 +75,74 @@ fn main(
57
  var acc{{ sfx }} = vec4<f32>(0.0);
58
  {% endfor %}
59
 
60
- // Stride whole bytes: every code a byte holds is consumed from one fetch.
61
- for (var k = tid * CODES; k < params.K; k = k + WG * CODES) {
62
- let block = k / params.blockSize;
63
- // blockSize is a power of two of at least 16, so a byte never straddles two
64
- // blocks and every code below shares this block's scale and zero point.
65
- let byte_index = (k % params.blockSize) / CODES;
66
- {% for j in range(codesPerByte) %}
67
- {% if j == 0 %}
68
- let av0 = f32(a[k]);
 
 
 
69
  {% else %}
70
- // K need not be a multiple of CODES; a code past the end contributes zero.
71
- let av{{ j }} = select(0.0, f32(a[min(k + {{ j }}u, params.K - 1u)]), k + {{ j }}u < params.K);
72
- {% endif %}
73
  {% endfor %}
 
 
74
  {% for g in range(groups) %}
75
  {% set sfx = "" if groups == 1 else g %}
76
  {% for c in range(4) %}
77
  {% set i = g * 4 + c %}
78
  {% if i == 0 %}
79
- let scale0 = f32(scales[(col_base + 0u) * params.kBlocks + block]);
80
- let zero0 = zero_point({% if hasZero %}col_base + 0u, block{% endif %});
81
- let word0 = packed_byte(col_base + 0u, block, byte_index);
82
- {% for j in range(codesPerByte) %}
83
- acc{{ sfx }}.x = acc{{ sfx }}.x
84
- + av{{ j }} * (f32((word0 >> {{ j * bits }}u) & {{ codeMask }}u) - zero0) * scale0;
85
- {% endfor %}
86
  {% else %}
87
  if (col_base + {{ i }}u < params.N) {
88
- let scale{{ i }} = f32(scales[(col_base + {{ i }}u) * params.kBlocks + block]);
89
- let zero{{ i }} = zero_point({% if hasZero %}col_base + {{ i }}u, block{% endif %});
90
- let word{{ i }} = packed_byte(col_base + {{ i }}u, block, byte_index);
91
- {% for j in range(codesPerByte) %}
92
- acc{{ sfx }}.{{ comps[c] }} = acc{{ sfx }}.{{ comps[c] }}
93
- + av{{ j }} * (f32((word{{ i }} >> {{ j * bits }}u) & {{ codeMask }}u) - zero{{ i }}) * scale{{ i }};
94
- {% endfor %}
95
- }
96
  {% endif %}
 
 
 
 
 
 
 
 
97
  {% endfor %}
98
  {% endfor %}
99
  }
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  {% for g in range(groups) %}
102
  {% set sfx = "" if groups == 1 else g %}
103
  partials{{ sfx }}[tid] = acc{{ sfx }};
@@ -124,6 +169,7 @@ fn main(
124
  {% set sfx = "" if groups == 1 else g %}
125
  let total{{ sfx }} = partials{{ sfx }}[0];
126
  {% endfor %}
 
127
  {% for g in range(groups) %}
128
  {% set sfx = "" if groups == 1 else g %}
129
  {% for c in range(4) %}
 
1
+ {% if useSubgroups %}
2
+ enable subgroups;
3
  {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
+ // com.microsoft.MatMulNBits decode GEMV: one activation row against the packed
6
+ // weight blob. A workgroup owns gemvNCols output columns; its lanes stride the
7
+ // reduction axis in whole VECTORS of the blob -- a vec4<u32> of sixteen packed
8
+ // bytes when the quantization block is a multiple of sixteen bytes, else one
9
+ // u32 word -- so every weight byte arrives in the widest load the layout admits.
10
+ // A vector never straddles a block, so one scale (and zero point) covers every
11
+ // code it carries.
12
+ //
13
+ // Per vector and column the lane accumulates dot(a, codes) over the vector's
14
+ // codes and applies the block's (scale, zero) once:
15
+ // sum_j a_j (c_j - z) s == s (dot(a, c) - z sum_j a_j)
16
+ // so the per-code work is a shift, a mask, a convert and one fma, and the
17
+ // activation sum is shared by every column. Partials fold through workgroup
18
+ // memory, so no subgroup feature is required.
19
  {% set nCols = gemvNCols %}
20
  {% set groups = 2 if nCols == 8 else 1 %}
21
  {% set comps = ["x", "y", "z", "w"] %}
 
 
22
 
23
  const WG: u32 = {{ workgroupSize }}u;
24
  const N_COLS: u32 = {{ nCols }}u;
25
+ const VEC_PER_COL: u32 = {{ vecPerCol }}u;
26
+ const VEC_PER_BLOCK: u32 = {{ vecPerBlock }}u;
27
+ {% if actVec4 %}
28
+ const VEC4_PER_VEC: u32 = {{ codesPerVec4 }}u;
29
+ {% else %}
30
+ const CODES_PER_VEC: u32 = {{ codesPerVec }}u;
31
+ {% endif %}
32
 
33
  {% for g in range(groups) %}
34
  {% set sfx = "" if groups == 1 else g %}
35
  var<workgroup> partials{{ sfx }}: array<vec4<f32>, {{ workgroupSize }}>;
36
  {% endfor %}
37
 
 
 
 
 
 
 
 
38
  // Per-(column, block) zero point. Symmetric weights use the baked default
39
+ // ({{ defaultZero }}); asymmetric weights carry one widened value per (col,
40
+ // block) in row-major [N, kBlocks] layout.
41
  fn zero_point({% if hasZero %}n: u32, block: u32{% endif %}) -> f32 {
42
  {% if hasZero %}
43
  return f32(zero_points[n * params.kBlocks + block]);
 
46
  {% endif %}
47
  }
48
 
49
+ {% macro vec_dot(words) %}
50
+ {% for w in range(vecWords) %}
51
+ {% set word = (words ~ "." ~ comps[w]) if vecWords == 4 else words %}
52
+ {% for h in range(codesPerWord) %}
53
+ dot = dot + a{{ w * codesPerWord + h }} * f32(({{ word }} >> {{ h * bits }}u) & {{ codeMask }}u);
54
+ {% endfor %}
55
+ {% endfor %}
56
+ {%- endmacro %}
57
+
58
  @compute @workgroup_size({{ workgroupSize }}, 1, 1)
59
  fn main(
60
  @builtin(workgroup_id) wid: vec3<u32>,
61
+ @builtin(local_invocation_id) lid: vec3<u32>{% if useSubgroups %},
62
+ @builtin(subgroup_invocation_id) sgLane: u32,
63
+ @builtin(subgroup_size) sgSize: u32{% endif %}
64
  ) {
65
  let tid = lid.x;
66
  // 2D-folded dispatch: wid.y carries the high bits past the per-dimension
67
  // limit (vocab-sized N). The fold over-dispatches, so the tail workgroups
68
  // return before any barrier.
69
+ let col_base = (wid.x + wid.y * {{ DISPATCH_FOLD_WIDTH }}u) * N_COLS;
70
  if (col_base >= params.N) {
71
  return;
72
  }
 
75
  var acc{{ sfx }} = vec4<f32>(0.0);
76
  {% endfor %}
77
 
78
+ for (var v = tid; v < VEC_PER_COL; v = v + WG) {
79
+ let block = v / VEC_PER_BLOCK;
80
+ {% if actVec4 %}
81
+ let abase = v * VEC4_PER_VEC;
82
+ {% for q in range(codesPerVec4) %}
83
+ let av{{ q }} = vec4<f32>(a[abase + {{ q }}u]);
84
+ {% endfor %}
85
+ {% for q in range(codesPerVec4) %}
86
+ {% for c in range(4) %}
87
+ let a{{ q * 4 + c }} = av{{ q }}.{{ comps[c] }};
88
+ {% endfor %}
89
+ {% endfor %}
90
  {% else %}
91
+ let k0 = v * CODES_PER_VEC;
92
+ {% for j in range(codesPerVec) %}
93
+ let a{{ j }} = select(0.0, f32(a[min(k0 + {{ j }}u, params.K - 1u)]), k0 + {{ j }}u < params.K);
94
  {% endfor %}
95
+ {% endif %}
96
+ let asum = {% for j in range(codesPerVec) %}{{ " + " if j > 0 else "" }}a{{ j }}{% endfor %};
97
  {% for g in range(groups) %}
98
  {% set sfx = "" if groups == 1 else g %}
99
  {% for c in range(4) %}
100
  {% set i = g * 4 + c %}
101
  {% if i == 0 %}
102
+ {
 
 
 
 
 
 
103
  {% else %}
104
  if (col_base + {{ i }}u < params.N) {
 
 
 
 
 
 
 
 
105
  {% endif %}
106
+ let n = col_base + {{ i }}u;
107
+ let words = b[n * VEC_PER_COL + v];
108
+ let scale = f32(scales[n * params.kBlocks + block]);
109
+ let zero = zero_point({% if hasZero %}n, block{% endif %});
110
+ var dot = 0.0;
111
+ {{ vec_dot("words") }}
112
+ acc{{ sfx }}.{{ comps[c] }} = acc{{ sfx }}.{{ comps[c] }} + (dot - zero * asum) * scale;
113
+ }
114
  {% endfor %}
115
  {% endfor %}
116
  }
117
 
118
+ {% if useSubgroups %}
119
+ // Subgroup fold: one collective per accumulator, then the WG / subgroup-size
120
+ // per-subgroup partials fold once through workgroup memory. The lanes of a
121
+ // subgroup are contiguous in local_invocation_id, so tid / sgSize names it.
122
+ // Every lane reaches this point (the only early return is workgroup-uniform),
123
+ // so the collective runs in uniform control flow.
124
+ {% for g in range(groups) %}
125
+ {% set sfx = "" if groups == 1 else g %}
126
+ let sgSum{{ sfx }} = subgroupAdd(acc{{ sfx }});
127
+ {% endfor %}
128
+ if (sgLane == 0u) {
129
+ {% for g in range(groups) %}
130
+ {% set sfx = "" if groups == 1 else g %}
131
+ partials{{ sfx }}[tid / sgSize] = sgSum{{ sfx }};
132
+ {% endfor %}
133
+ }
134
+ workgroupBarrier();
135
+
136
+ if (tid == 0u) {
137
+ let subgroupCount = WG / sgSize;
138
+ {% for g in range(groups) %}
139
+ {% set sfx = "" if groups == 1 else g %}
140
+ var total{{ sfx }} = partials{{ sfx }}[0];
141
+ for (var i = 1u; i < subgroupCount; i = i + 1u) {
142
+ total{{ sfx }} = total{{ sfx }} + partials{{ sfx }}[i];
143
+ }
144
+ {% endfor %}
145
+ {% else %}
146
  {% for g in range(groups) %}
147
  {% set sfx = "" if groups == 1 else g %}
148
  partials{{ sfx }}[tid] = acc{{ sfx }};
 
169
  {% set sfx = "" if groups == 1 else g %}
170
  let total{{ sfx }} = partials{{ sfx }}[0];
171
  {% endfor %}
172
+ {% endif %}
173
  {% for g in range(groups) %}
174
  {% set sfx = "" if groups == 1 else g %}
175
  {% for c in range(4) %}
build/webgpu/matmul-nbits-q4-dp4a-prefill.wgsl.jinja CHANGED
@@ -53,11 +53,14 @@ fn main(
53
  tA[r][w] = select(0u, a_quant[am * K4 + kt * 8u + w], am < M);
54
  let bn = nBase + r;
55
  if (bn < N) {
56
- // Two blob bytes hold this word's four sequential weights
57
- // (low nibble = even k). Rebias by the implicit zero point 8.
 
 
58
  let byteBase = (bn * KBLOCKS + block) * BLOB_SIZE + ((kBase % BLOCK_SIZE) + w * 4u) / 2u;
59
- let b0 = b[byteBase];
60
- let b1 = b[byteBase + 1u];
 
61
  let quads = vec4<i32>(i32(b0 & 15u), i32((b0 >> 4u) & 15u), i32(b1 & 15u), i32((b1 >> 4u) & 15u));
62
  tB[r][w] = pack4xI8(quads - vec4<i32>(8));
63
  } else {
 
53
  tA[r][w] = select(0u, a_quant[am * K4 + kt * 8u + w], am < M);
54
  let bn = nBase + r;
55
  if (bn < N) {
56
+ // Two blob bytes hold this word's four sequential weights (low nibble =
57
+ // even k). The blob is packed four bytes per u32 word, and an even byte
58
+ // index inside a 4-byte-aligned blob never straddles a word, so both
59
+ // bytes come from one load. Rebias by the implicit zero point 8.
60
  let byteBase = (bn * KBLOCKS + block) * BLOB_SIZE + ((kBase % BLOCK_SIZE) + w * 4u) / 2u;
61
+ let pair = b[byteBase >> 2u] >> ((byteBase & 3u) * 8u);
62
+ let b0 = pair & 255u;
63
+ let b1 = (pair >> 8u) & 255u;
64
  let quads = vec4<i32>(i32(b0 & 15u), i32((b0 >> 4u) & 15u), i32(b1 & 15u), i32((b1 >> 4u) & 15u));
65
  tB[r][w] = pack4xI8(quads - vec4<i32>(8));
66
  } else {
build/webgpu/matmul-nbits-q4-prefill-tile4x4.wgsl.jinja CHANGED
@@ -1,38 +1,28 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
-
6
- const WG: u32 = {{ workgroupSize }}u;
 
 
 
 
 
 
 
 
7
  const TILE_M: u32 = 4u;
8
  const TILE_N: u32 = 4u;
 
 
 
 
 
 
9
 
10
  var<workgroup> partial0: array<vec4<f32>, {{ workgroupSize }}>;
11
  var<workgroup> partial1: array<vec4<f32>, {{ workgroupSize }}>;
12
  var<workgroup> partial2: array<vec4<f32>, {{ workgroupSize }}>;
13
  var<workgroup> partial3: array<vec4<f32>, {{ workgroupSize }}>;
14
 
15
- // {{ bits }}-bit unpack: q2 stores four codes per byte, q4 two, and q8 one.
16
- {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
17
- fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
18
- {% if bits == 2 %}
19
- let byte_index = offset / 4u;
20
- let shift = (offset % 4u) * 2u;
21
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
22
- return ({{ buffer }}[packed_index] >> shift) & 3u;
23
- {% elif bits == 4 %}
24
- let byte_index = offset / 2u;
25
- let shift = (offset % 2u) * 4u;
26
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
27
- return ({{ buffer }}[packed_index] >> shift) & 15u;
28
- {% else %}
29
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
30
- return {{ buffer }}[packed_index] & 255u;
31
- {% endif %}
32
- }
33
- {%- endmacro %}
34
- {{- matmul_nbits_packed_code(bits=bits) }}
35
-
36
  fn store_row(row: u32, col_base: u32, value: vec4<f32>) {
37
  if (row >= params.rows) {
38
  return;
@@ -54,78 +44,103 @@ fn main(
54
  @builtin(num_workgroups) nwg: vec3<u32>
55
  ) {
56
  let tid = lid.x;
 
 
57
  // Column tiles fold into z past the per-dimension limit (vocab-sized N,
58
  // where y already carries the row-tile grid-stride). Over-dispatched tail
59
- // workgroups return before any barrier.
60
- let col_base = (wid.x + wid.z * nwg.x) * TILE_N;
61
- if (col_base >= params.N) {
 
 
62
  return;
63
  }
 
 
 
 
 
64
 
65
  // Grid-stride over row tiles because dispatch y is device-clamped. This loop
66
  // reduces to one iteration when the dispatch covers every tile directly.
67
  for (var row_tile = wid.y; row_tile * TILE_M < params.rows; row_tile = row_tile + nwg.y) {
68
  let row_base = row_tile * TILE_M;
 
 
 
 
 
 
 
 
 
69
 
70
  var acc0 = vec4<f32>(0.0);
71
  var acc1 = vec4<f32>(0.0);
72
  var acc2 = vec4<f32>(0.0);
73
  var acc3 = vec4<f32>(0.0);
74
 
75
- for (var block: u32 = 0u; block < params.kBlocks; block = block + 1u) {
76
- let scale = vec4<f32>(
77
- f32(scales[(col_base + 0u) * params.kBlocks + block]),
78
- f32(scales[(col_base + 1u) * params.kBlocks + block]),
79
- f32(scales[(col_base + 2u) * params.kBlocks + block]),
80
- f32(scales[(col_base + 3u) * params.kBlocks + block])
81
- );
82
- let block_k = block * params.blockSize;
83
-
84
- for (var offset: u32 = tid; offset < params.blockSize; offset = offset + WG) {
85
- let k = block_k + offset;
 
 
 
86
  {% if hasZero %}
87
- let zero = vec4<f32>(
88
- f32(zero_points[(col_base + 0u) * params.kBlocks + block]),
89
- f32(zero_points[(col_base + 1u) * params.kBlocks + block]),
90
- f32(zero_points[(col_base + 2u) * params.kBlocks + block]),
91
- f32(zero_points[(col_base + 3u) * params.kBlocks + block])
92
- );
93
- {% else %}
94
- let zero = vec4<f32>({{ defaultZero }});
95
  {% endif %}
96
- let w = (vec4<f32>(
97
- f32(packed_weight(col_base + 0u, block, offset)),
98
- f32(packed_weight(col_base + 1u, block, offset)),
99
- f32(packed_weight(col_base + 2u, block, offset)),
100
- f32(packed_weight(col_base + 3u, block, offset))
101
- ) - zero) * scale;
102
-
103
- var a0 = 0.0;
104
- var a1 = 0.0;
105
- var a2 = 0.0;
106
- var a3 = 0.0;
107
- if (row_base + 0u < params.rows) {
108
- a0 = f32(a[(row_base + 0u) * params.K + k]);
109
- }
110
- if (row_base + 1u < params.rows) {
111
- a1 = f32(a[(row_base + 1u) * params.K + k]);
112
  }
113
- if (row_base + 2u < params.rows) {
114
- a2 = f32(a[(row_base + 2u) * params.K + k]);
 
 
 
 
115
  }
116
- if (row_base + 3u < params.rows) {
117
- a3 = f32(a[(row_base + 3u) * params.K + k]);
 
 
 
 
118
  }
119
-
120
- acc0 = acc0 + a0 * w;
121
- acc1 = acc1 + a1 * w;
122
- acc2 = acc2 + a2 * w;
123
- acc3 = acc3 + a3 * w;
124
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  }
126
 
127
  // Sync before reusing the shared partial buffers: on a subsequent grid-stride
128
- // iteration the previous iteration's tid==0 store may still be reading them.
129
  workgroupBarrier();
130
  partial0[tid] = acc0;
131
  partial1[tid] = acc1;
@@ -133,38 +148,35 @@ fn main(
133
  partial3[tid] = acc3;
134
  workgroupBarrier();
135
 
136
- var stride = WG / 2u;
137
- loop {
138
- if (stride == 0u) {
139
- break;
140
- }
141
- if (tid < stride) {
142
  partial0[tid] = partial0[tid] + partial0[tid + stride];
143
  partial1[tid] = partial1[tid] + partial1[tid + stride];
144
  partial2[tid] = partial2[tid] + partial2[tid + stride];
145
  partial3[tid] = partial3[tid] + partial3[tid + stride];
146
  }
147
- stride = stride / 2u;
148
  workgroupBarrier();
149
  }
150
 
151
- if (tid == 0u) {
152
  {% if hasBias %}
153
  let biasv = vec4<f32>(
154
  f32(bias[col_base + 0u]),
155
- f32(bias[col_base + 1u]),
156
- f32(bias[col_base + 2u]),
157
- f32(bias[col_base + 3u])
158
  );
159
- store_row(row_base + 0u, col_base, partial0[0] + biasv);
160
- store_row(row_base + 1u, col_base, partial1[0] + biasv);
161
- store_row(row_base + 2u, col_base, partial2[0] + biasv);
162
- store_row(row_base + 3u, col_base, partial3[0] + biasv);
163
  {% else %}
164
- store_row(row_base + 0u, col_base, partial0[0]);
165
- store_row(row_base + 1u, col_base, partial1[0]);
166
- store_row(row_base + 2u, col_base, partial2[0]);
167
- store_row(row_base + 3u, col_base, partial3[0]);
168
  {% endif %}
169
  }
170
  }
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
+ // com.microsoft.MatMulNBits small-M prefill over the packed blob: any M >= 2, K
3
+ // a whole number of quantization blocks. A workgroup covers TILE_M = 4
4
+ // activation rows by TILE_N * COL_GROUPS output columns. Its lanes split into
5
+ // COL_GROUPS groups of K_LANES; a group owns four adjacent columns and its
6
+ // lanes advance through the blob one packed u32 word (32 / bits codes) at a
7
+ // time. Each lane retains the four columns' words while applying them to four
8
+ // activation rows, and one (scale, zero) pair per column serves every code in
9
+ // the word. A word never straddles a quantization block.
10
+ // Partials fold inside each column group through workgroup memory (portable:
11
+ // no subgroup feature). K_LANES shrinks with the words per column for short K.
12
  const TILE_M: u32 = 4u;
13
  const TILE_N: u32 = 4u;
14
+ const K_LANES: u32 = {{ kLanes }}u;
15
+ const COL_GROUPS: u32 = {{ colGroups }}u;
16
+ const COLS_PER_WORKGROUP: u32 = TILE_N * COL_GROUPS;
17
+ const WORDS_PER_COL: u32 = {{ wordsPerCol }}u;
18
+ const WORDS_PER_BLOCK: u32 = {{ wordsPerBlock }}u;
19
+ const CODES_PER_WORD: u32 = {{ codesPerWord }}u;
20
 
21
  var<workgroup> partial0: array<vec4<f32>, {{ workgroupSize }}>;
22
  var<workgroup> partial1: array<vec4<f32>, {{ workgroupSize }}>;
23
  var<workgroup> partial2: array<vec4<f32>, {{ workgroupSize }}>;
24
  var<workgroup> partial3: array<vec4<f32>, {{ workgroupSize }}>;
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  fn store_row(row: u32, col_base: u32, value: vec4<f32>) {
27
  if (row >= params.rows) {
28
  return;
 
44
  @builtin(num_workgroups) nwg: vec3<u32>
45
  ) {
46
  let tid = lid.x;
47
+ let group = tid / K_LANES;
48
+ let kl = tid % K_LANES;
49
  // Column tiles fold into z past the per-dimension limit (vocab-sized N,
50
  // where y already carries the row-tile grid-stride). Over-dispatched tail
51
+ // workgroups return before any barrier; a column GROUP past N inside a live
52
+ // workgroup keeps running on zeroed loads so the barriers stay uniform, and
53
+ // its store is skipped.
54
+ let workgroup_col = (wid.x + wid.z * {{ DISPATCH_FOLD_WIDTH }}u) * COLS_PER_WORKGROUP;
55
+ if (workgroup_col >= params.N) {
56
  return;
57
  }
58
+ let col_base = workgroup_col + group * TILE_N;
59
+ let group_valid = col_base < params.N;
60
+ let col1 = col_base + 1u < params.N;
61
+ let col2 = col_base + 2u < params.N;
62
+ let col3 = col_base + 3u < params.N;
63
 
64
  // Grid-stride over row tiles because dispatch y is device-clamped. This loop
65
  // reduces to one iteration when the dispatch covers every tile directly.
66
  for (var row_tile = wid.y; row_tile * TILE_M < params.rows; row_tile = row_tile + nwg.y) {
67
  let row_base = row_tile * TILE_M;
68
+ // Rows past the end clamp onto the last real row; their accumulators are
69
+ // finite and the store guard drops them.
70
+ let row1 = row_base + 1u < params.rows;
71
+ let row2 = row_base + 2u < params.rows;
72
+ let row3 = row_base + 3u < params.rows;
73
+ let abase0 = row_base * params.K;
74
+ let abase1 = min(row_base + 1u, params.rows - 1u) * params.K;
75
+ let abase2 = min(row_base + 2u, params.rows - 1u) * params.K;
76
+ let abase3 = min(row_base + 3u, params.rows - 1u) * params.K;
77
 
78
  var acc0 = vec4<f32>(0.0);
79
  var acc1 = vec4<f32>(0.0);
80
  var acc2 = vec4<f32>(0.0);
81
  var acc3 = vec4<f32>(0.0);
82
 
83
+ for (var w = kl; w < WORDS_PER_COL; w = w + K_LANES) {
84
+ let block = w / WORDS_PER_BLOCK;
85
+ let k0 = w * CODES_PER_WORD;
86
+ // One packed word per column, plus its block's scale and zero point. A
87
+ // column past N reads nothing and dequantizes to zero (scale 0).
88
+ var q0 = 0u;
89
+ var q1 = 0u;
90
+ var q2 = 0u;
91
+ var q3 = 0u;
92
+ var scale = vec4<f32>(0.0);
93
+ var zero = vec4<f32>({{ defaultZero }});
94
+ if (group_valid) {
95
+ q0 = b[(col_base + 0u) * WORDS_PER_COL + w];
96
+ scale.x = f32(scales[(col_base + 0u) * params.kBlocks + block]);
97
  {% if hasZero %}
98
+ zero.x = f32(zero_points[(col_base + 0u) * params.kBlocks + block]);
99
+ {% endif %}
100
+ if (col1) {
101
+ q1 = b[(col_base + 1u) * WORDS_PER_COL + w];
102
+ scale.y = f32(scales[(col_base + 1u) * params.kBlocks + block]);
103
+ {% if hasZero %}
104
+ zero.y = f32(zero_points[(col_base + 1u) * params.kBlocks + block]);
 
105
  {% endif %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
+ if (col2) {
108
+ q2 = b[(col_base + 2u) * WORDS_PER_COL + w];
109
+ scale.z = f32(scales[(col_base + 2u) * params.kBlocks + block]);
110
+ {% if hasZero %}
111
+ zero.z = f32(zero_points[(col_base + 2u) * params.kBlocks + block]);
112
+ {% endif %}
113
  }
114
+ if (col3) {
115
+ q3 = b[(col_base + 3u) * WORDS_PER_COL + w];
116
+ scale.w = f32(scales[(col_base + 3u) * params.kBlocks + block]);
117
+ {% if hasZero %}
118
+ zero.w = f32(zero_points[(col_base + 3u) * params.kBlocks + block]);
119
+ {% endif %}
120
  }
 
 
 
 
 
121
  }
122
+ {% for j in range(codesPerWord) %}
123
+ {
124
+ let wt = (vec4<f32>(
125
+ f32((q0 >> {{ j * bits }}u) & {{ codeMask }}u),
126
+ f32((q1 >> {{ j * bits }}u) & {{ codeMask }}u),
127
+ f32((q2 >> {{ j * bits }}u) & {{ codeMask }}u),
128
+ f32((q3 >> {{ j * bits }}u) & {{ codeMask }}u)) - zero) * scale;
129
+ let k = k0 + {{ j }}u;
130
+ let a0 = f32(a[abase0 + k]);
131
+ let a1 = select(0.0, f32(a[abase1 + k]), row1);
132
+ let a2 = select(0.0, f32(a[abase2 + k]), row2);
133
+ let a3 = select(0.0, f32(a[abase3 + k]), row3);
134
+ acc0 = acc0 + a0 * wt;
135
+ acc1 = acc1 + a1 * wt;
136
+ acc2 = acc2 + a2 * wt;
137
+ acc3 = acc3 + a3 * wt;
138
+ }
139
+ {% endfor %}
140
  }
141
 
142
  // Sync before reusing the shared partial buffers: on a subsequent grid-stride
143
+ // iteration the previous iteration's group-leader store may still be reading.
144
  workgroupBarrier();
145
  partial0[tid] = acc0;
146
  partial1[tid] = acc1;
 
148
  partial3[tid] = acc3;
149
  workgroupBarrier();
150
 
151
+ // Fold within each column group. K_LANES is a power of two, so kl + stride
152
+ // never leaves the group.
153
+ for (var stride = K_LANES / 2u; stride > 0u; stride = stride / 2u) {
154
+ if (kl < stride) {
 
 
155
  partial0[tid] = partial0[tid] + partial0[tid + stride];
156
  partial1[tid] = partial1[tid] + partial1[tid + stride];
157
  partial2[tid] = partial2[tid] + partial2[tid + stride];
158
  partial3[tid] = partial3[tid] + partial3[tid + stride];
159
  }
 
160
  workgroupBarrier();
161
  }
162
 
163
+ if (kl == 0u && group_valid) {
164
  {% if hasBias %}
165
  let biasv = vec4<f32>(
166
  f32(bias[col_base + 0u]),
167
+ select(0.0, f32(bias[min(col_base + 1u, params.N - 1u)]), col1),
168
+ select(0.0, f32(bias[min(col_base + 2u, params.N - 1u)]), col2),
169
+ select(0.0, f32(bias[min(col_base + 3u, params.N - 1u)]), col3)
170
  );
171
+ store_row(row_base + 0u, col_base, partial0[tid] + biasv);
172
+ store_row(row_base + 1u, col_base, partial1[tid] + biasv);
173
+ store_row(row_base + 2u, col_base, partial2[tid] + biasv);
174
+ store_row(row_base + 3u, col_base, partial3[tid] + biasv);
175
  {% else %}
176
+ store_row(row_base + 0u, col_base, partial0[tid]);
177
+ store_row(row_base + 1u, col_base, partial1[tid]);
178
+ store_row(row_base + 2u, col_base, partial2[tid]);
179
+ store_row(row_base + 3u, col_base, partial3[tid]);
180
  {% endif %}
181
  }
182
  }
build/webgpu/matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  // com.microsoft.MatMulNBits q4/q8 prefill, no-subgroup-matrix tier — register-blocked.
@@ -30,6 +27,13 @@ const TN: u32 = {{ threadCols }}u;
30
  const WG_X: u32 = 16u;
31
  const WG_Y: u32 = 16u;
32
  const WG_THREADS: u32 = WG_X * WG_Y;
 
 
 
 
 
 
 
33
 
34
  // When A uses f16 storage, staging tileA at f16 is bit-identical to widening it
35
  // here. tileB stays f32 because the dequantized (code - zero)*scale product
@@ -46,18 +50,16 @@ var<workgroup> tileB: array<array<vec4<f32>, K_VECS>, BN>; // [BN][BK/4]
46
  {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
47
  fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
48
  {% if bits == 2 %}
49
- let byte_index = offset / 4u;
50
- let shift = (offset % 4u) * 2u;
51
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
52
- return ({{ buffer }}[packed_index] >> shift) & 3u;
53
  {% elif bits == 4 %}
54
- let byte_index = offset / 2u;
55
- let shift = (offset % 2u) * 4u;
56
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
57
- return ({{ buffer }}[packed_index] >> shift) & 15u;
58
  {% else %}
59
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
60
- return {{ buffer }}[packed_index] & 255u;
61
  {% endif %}
62
  }
63
  {%- endmacro %}
@@ -82,7 +84,13 @@ fn main(
82
  {% endfor %}
83
 
84
  let numTiles = (K + BK - 1u) / BK;
 
 
 
 
 
85
  for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
 
86
  let kBase = kt * BK;
87
  // A and B may have different tile areas (32x64 portable geometry), so stage
88
  // them independently. Every configured area is an exact WG_THREADS multiple.
@@ -92,17 +100,26 @@ fn main(
92
  let am = mBase + ar;
93
  let ak = kBase + ac4 * 4u;
94
  var aWord = vec4<{{ aTile }}>({{ aTile }}(0.0));
 
 
 
 
 
 
95
  if (am < M) {
96
  {% for component in range(4) %}
97
  if (ak + {{ component }}u < K) { aWord[{{ component }}u] = {{ aTile }}(a[am * K + ak + {{ component }}u]); }
98
  {% endfor %}
99
  }
 
100
  tileA[ar][ac4] = aWord;
101
  }
102
  {% if alignedBlockLoads %}
103
  // Every lane materializes one vector word of one output column. BLOCK_SIZE is
104
  // a multiple of BK, so the whole tile slice of a column shares one scale and
105
- // zero point and each stored byte is read once for the codes it carries.
 
 
106
  for (var idx: u32 = li; idx < BN * K_VECS; idx = idx + WG_THREADS) {
107
  let bc = idx / K_VECS;
108
  let kv = idx % K_VECS;
@@ -116,9 +133,9 @@ fn main(
116
  let blob_base = (bn * KBLOCKS + block) * BLOB_SIZE;
117
  let kk = kv * 4u;
118
  {% if bits == 2 %}
119
- // Four 2-bit codes share a stored byte, and an aligned tile starts a
120
- // word on a byte boundary, so one byte fills one vector word.
121
- let packed = b[blob_base + (offset0 + kk) / 4u];
122
  {% for component in range(4) %}
123
  if (kBase + kk + {{ component }}u < K) {
124
  word[{{ component }}u] = (f32((packed >> {{ component * 2 }}u) & 3u) - zero) * scale;
@@ -126,19 +143,19 @@ fn main(
126
  {% endfor %}
127
  {% elif bits == 4 %}
128
  // Two bytes carry the four K-adjacent codes of one vector word.
129
- let byte_index = blob_base + (offset0 + kk) / 2u;
130
- let packed0 = b[byte_index];
131
- let packed1 = b[byte_index + 1u];
132
  {% for component in range(4) %}
133
  if (kBase + kk + {{ component }}u < K) {
134
- word[{{ component }}u] = (f32(({{ "packed0" if component < 2 else "packed1" }} >> {{ (component % 2) * 4 }}u) & 15u) - zero) * scale;
135
  }
136
  {% endfor %}
137
  {% else %}
138
  let byte_index = blob_base + offset0 + kk;
 
139
  {% for component in range(4) %}
140
  if (kBase + kk + {{ component }}u < K) {
141
- word[{{ component }}u] = (f32(b[byte_index + {{ component }}u] & 255u) - zero) * scale;
142
  }
143
  {% endfor %}
144
  {% endif %}
@@ -192,7 +209,11 @@ fn main(
192
  if (m0 + {{ i }}u < M) {
193
  {% for j in range(threadCols) %}
194
  if (n0 + {{ j }}u < N) {
 
 
 
195
  y[(m0 + {{ i }}u) * N + n0 + {{ j }}u] = {{ outputScalar }}(acc{{ i }}{{ j }}{% if hasBias %} + f32(bias[n0 + {{ j }}u]){% endif %});
 
196
  }
197
  {% endfor %}
198
  }
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  // com.microsoft.MatMulNBits q4/q8 prefill, no-subgroup-matrix tier — register-blocked.
 
27
  const WG_X: u32 = 16u;
28
  const WG_Y: u32 = 16u;
29
  const WG_THREADS: u32 = WG_X * WG_Y;
30
+ {% set splitKValue = splitK if splitK is defined else 1 %}
31
+ {% set tilesPerSplitValue = tilesPerSplit if tilesPerSplit is defined else 0 %}
32
+ {% if splitKValue > 1 %}
33
+ // Split-K: dispatch.z owns SPLIT_TILES consecutive K tiles and writes an f32
34
+ // partial block for the combine pass (bias, if any, is added there).
35
+ const SPLIT_TILES: u32 = {{ tilesPerSplitValue }}u;
36
+ {% endif %}
37
 
38
  // When A uses f16 storage, staging tileA at f16 is bit-identical to widening it
39
  // here. tileB stays f32 because the dequantized (code - zero)*scale product
 
50
  {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
51
  fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
52
  {% if bits == 2 %}
53
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 2u);
54
+ let shift = (byte_index & 3u) * 8u + (offset & 3u) * 2u;
55
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 3u;
 
56
  {% elif bits == 4 %}
57
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 1u);
58
+ let shift = (byte_index & 3u) * 8u + (offset & 1u) * 4u;
59
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 15u;
 
60
  {% else %}
61
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
62
+ return ({{ buffer }}[byte_index >> 2u] >> ((byte_index & 3u) * 8u)) & 255u;
63
  {% endif %}
64
  }
65
  {%- endmacro %}
 
84
  {% endfor %}
85
 
86
  let numTiles = (K + BK - 1u) / BK;
87
+ {% if splitKValue > 1 %}
88
+ let ktBegin = wg.z * SPLIT_TILES;
89
+ let ktEnd = min(numTiles, ktBegin + SPLIT_TILES);
90
+ for (var kt: u32 = ktBegin; kt < ktEnd; kt = kt + 1u) {
91
+ {% else %}
92
  for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
93
+ {% endif %}
94
  let kBase = kt * BK;
95
  // A and B may have different tile areas (32x64 portable geometry), so stage
96
  // them independently. Every configured area is an exact WG_THREADS multiple.
 
100
  let am = mBase + ar;
101
  let ak = kBase + ac4 * 4u;
102
  var aWord = vec4<{{ aTile }}>({{ aTile }}(0.0));
103
+ {% if aVec4Loads is defined and aVec4Loads %}
104
+ // K is a multiple of four, so every staged word is one aligned vector load.
105
+ if (am < M) {
106
+ aWord = vec4<{{ aTile }}>(a[(am * K + ak) / 4u]);
107
+ }
108
+ {% else %}
109
  if (am < M) {
110
  {% for component in range(4) %}
111
  if (ak + {{ component }}u < K) { aWord[{{ component }}u] = {{ aTile }}(a[am * K + ak + {{ component }}u]); }
112
  {% endfor %}
113
  }
114
+ {% endif %}
115
  tileA[ar][ac4] = aWord;
116
  }
117
  {% if alignedBlockLoads %}
118
  // Every lane materializes one vector word of one output column. BLOCK_SIZE is
119
  // a multiple of BK, so the whole tile slice of a column shares one scale and
120
+ // zero point. The blob is packed four bytes per u32 word and the four
121
+ // K-adjacent codes of a vector word are byte-contiguous from a 4-aligned
122
+ // offset, so they always come out of ONE packed word: one load per vector.
123
  for (var idx: u32 = li; idx < BN * K_VECS; idx = idx + WG_THREADS) {
124
  let bc = idx / K_VECS;
125
  let kv = idx % K_VECS;
 
133
  let blob_base = (bn * KBLOCKS + block) * BLOB_SIZE;
134
  let kk = kv * 4u;
135
  {% if bits == 2 %}
136
+ // Four 2-bit codes share a stored byte.
137
+ let byte_index = blob_base + ((offset0 + kk) >> 2u);
138
+ let packed = b[byte_index >> 2u] >> ((byte_index & 3u) * 8u);
139
  {% for component in range(4) %}
140
  if (kBase + kk + {{ component }}u < K) {
141
  word[{{ component }}u] = (f32((packed >> {{ component * 2 }}u) & 3u) - zero) * scale;
 
143
  {% endfor %}
144
  {% elif bits == 4 %}
145
  // Two bytes carry the four K-adjacent codes of one vector word.
146
+ let byte_index = blob_base + ((offset0 + kk) >> 1u);
147
+ let packed = b[byte_index >> 2u] >> ((byte_index & 3u) * 8u);
 
148
  {% for component in range(4) %}
149
  if (kBase + kk + {{ component }}u < K) {
150
+ word[{{ component }}u] = (f32((packed >> {{ component * 4 }}u) & 15u) - zero) * scale;
151
  }
152
  {% endfor %}
153
  {% else %}
154
  let byte_index = blob_base + offset0 + kk;
155
+ let packed = b[byte_index >> 2u];
156
  {% for component in range(4) %}
157
  if (kBase + kk + {{ component }}u < K) {
158
+ word[{{ component }}u] = (f32((packed >> {{ component * 8 }}u) & 255u) - zero) * scale;
159
  }
160
  {% endfor %}
161
  {% endif %}
 
209
  if (m0 + {{ i }}u < M) {
210
  {% for j in range(threadCols) %}
211
  if (n0 + {{ j }}u < N) {
212
+ {% if splitKValue > 1 %}
213
+ y[wg.z * M * N + (m0 + {{ i }}u) * N + n0 + {{ j }}u] = acc{{ i }}{{ j }};
214
+ {% else %}
215
  y[(m0 + {{ i }}u) * N + n0 + {{ j }}u] = {{ outputScalar }}(acc{{ i }}{{ j }}{% if hasBias %} + f32(bias[n0 + {{ j }}u]){% endif %});
216
+ {% endif %}
217
  }
218
  {% endfor %}
219
  }
build/webgpu/matmul-nbits-q4-prefill-tiled.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  // com.microsoft.MatMulNBits q4/q8 prefill, no-subgroup-matrix tier:
@@ -8,9 +5,9 @@ enable f16;
8
  // Register-blocked 32x32 shared-memory tiled GEMM (16x16 threads, each computes a
9
  // 2x2 micro-tile, K stepped in BK=16 chunks). The B operand is dequantized from
10
  // the packed blob straight into the shared B tile (no f32 weight matrix stored).
11
- // Each staged operand is reused across the micro-tile, and f32 accumulation is
12
- // exact for the 4/8-bit dequantized weight. Fully bounds-checked, so any M/N is
13
- // supported when K % blockSize == 0.
14
  const M: u32 = {{ M }}u;
15
  const K: u32 = {{ K }}u;
16
  const N: u32 = {{ N }}u;
@@ -34,18 +31,16 @@ var<workgroup> tileB: array<array<f32, 32>, 16>;
34
  {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
35
  fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
36
  {% if bits == 2 %}
37
- let byte_index = offset / 4u;
38
- let shift = (offset % 4u) * 2u;
39
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
40
- return ({{ buffer }}[packed_index] >> shift) & 3u;
41
  {% elif bits == 4 %}
42
- let byte_index = offset / 2u;
43
- let shift = (offset % 2u) * 4u;
44
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
45
- return ({{ buffer }}[packed_index] >> shift) & 15u;
46
  {% else %}
47
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
48
- return {{ buffer }}[packed_index] & 255u;
49
  {% endif %}
50
  }
51
  {%- endmacro %}
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  // com.microsoft.MatMulNBits q4/q8 prefill, no-subgroup-matrix tier:
 
5
  // Register-blocked 32x32 shared-memory tiled GEMM (16x16 threads, each computes a
6
  // 2x2 micro-tile, K stepped in BK=16 chunks). The B operand is dequantized from
7
  // the packed blob straight into the shared B tile (no f32 weight matrix stored).
8
+ // Each staged operand is reused across the micro-tile. Dequantized weights and
9
+ // accumulations use f32. Bounds checks support any M and N when K is divisible
10
+ // by blockSize.
11
  const M: u32 = {{ M }}u;
12
  const K: u32 = {{ K }}u;
13
  const N: u32 = {{ N }}u;
 
31
  {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
32
  fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
33
  {% if bits == 2 %}
34
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 2u);
35
+ let shift = (byte_index & 3u) * 8u + (offset & 3u) * 2u;
36
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 3u;
 
37
  {% elif bits == 4 %}
38
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 1u);
39
+ let shift = (byte_index & 3u) * 8u + (offset & 1u) * 4u;
40
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 15u;
 
41
  {% else %}
42
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
43
+ return ({{ buffer }}[byte_index >> 2u] >> ((byte_index & 3u) * 8u)) & 255u;
44
  {% endif %}
45
  }
46
  {%- endmacro %}
build/webgpu/matmul-nbits-q4-sgmat.wgsl.jinja CHANGED
@@ -2,9 +2,8 @@
2
  // Y[M,N] = A[M,K] @ W[K,N] + bias?, W[k,n] = (q4(n,block,offset) - zero) * scale
3
  // Uses a 32x64 or 64x64 subgroup-matrix output tile with an M-tail row_limit guard; the
4
  // B operand is materialized on the fly by dequantizing the packed q4 blob into
5
- // the shared B tile, so no f32 weight matrix is stored. f32 operands and f32
6
- // accumulation preserve precision; the dequantized weight is a 4-bit value, so
7
- // f32 is exact for it.
8
  // Alignment gates keep K % 32 == 0 and N % 64 == 0 so subgroupMatrixLoad never
9
  // sees partial 8x8 tiles; M tails handled by row_limit + the loadSHMA zero-fill.
10
  enable subgroups;
@@ -12,11 +11,9 @@ enable subgroups;
12
  enable subgroup_size_control;
13
  {% endif %}
14
  enable chromium_experimental_subgroup_matrix;
15
- {% if usesF16 %}
16
- enable f16;
17
- {% endif %}
18
  diagnostic(off, chromium.subgroup_matrix_uniformity);
19
 
 
20
  {{ env.wgsl.resourceDeclarations }}
21
 
22
  const M: u32 = {{ M }}u;
@@ -68,25 +65,31 @@ fn loadSHMB(tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
68
  {% else %}
69
  let zero = {{ defaultZero }};
70
  {% endif %}
 
 
 
 
71
  {% if bits == 2 %}
72
- // Four 2-bit codes share a stored byte; one code per iteration needs no assumption
73
- // about where offset0 lands inside a byte.
74
  for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
75
  let code_offset = offset0 + i;
76
- let packed_index = (n * KBLOCKS + block) * BLOB_SIZE + code_offset / 4u;
77
- tile_B[row * TILE_K + col + i] = (f32((b[packed_index] >> ((code_offset % 4u) * 2u)) & 3u) - zero) * scale;
 
78
  }
79
  {% elif bits == 4 %}
80
- for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 2u) {
81
- let packed_index = (n * KBLOCKS + block) * BLOB_SIZE + (offset0 + i) / 2u;
82
- let packed = b[packed_index];
83
- tile_B[row * TILE_K + col + i] = (f32(packed & 15u) - zero) * scale;
84
- tile_B[row * TILE_K + col + i + 1u] = (f32((packed >> 4u) & 15u) - zero) * scale;
85
  }
86
  {% else %}
87
- for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
88
- let packed_index = (n * KBLOCKS + block) * BLOB_SIZE + offset0 + i;
89
- tile_B[row * TILE_K + col + i] = (f32(b[packed_index] & 255u) - zero) * scale;
 
 
90
  }
91
  {% endif %}
92
  } else {
@@ -147,15 +150,16 @@ fn main(
147
  workgroupBarrier();
148
 
149
  for (var step = 0u; step < TILE_K; step = step + 8u) {
150
- {% set operandScalar = "f32" %} let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
151
- var matA0: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset, TILE_K);
152
- var matA1: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset + 8u * TILE_K, TILE_K);
 
 
153
 
154
  let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
155
- var matB0: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset, TILE_K);
156
- var matB1: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset + 8u * TILE_K, TILE_K);
157
- var matB2: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset + 16u * TILE_K, TILE_K);
158
- var matB3: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset + 24u * TILE_K, TILE_K);
159
 
160
  matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
161
  matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
 
2
  // Y[M,N] = A[M,K] @ W[K,N] + bias?, W[k,n] = (q4(n,block,offset) - zero) * scale
3
  // Uses a 32x64 or 64x64 subgroup-matrix output tile with an M-tail row_limit guard; the
4
  // B operand is materialized on the fly by dequantizing the packed q4 blob into
5
+ // the shared B tile, so no f32 weight matrix is stored. Dequantized operands and
6
+ // accumulations use f32.
 
7
  // Alignment gates keep K % 32 == 0 and N % 64 == 0 so subgroupMatrixLoad never
8
  // sees partial 8x8 tiles; M tails handled by row_limit + the loadSHMA zero-fill.
9
  enable subgroups;
 
11
  enable subgroup_size_control;
12
  {% endif %}
13
  enable chromium_experimental_subgroup_matrix;
 
 
 
14
  diagnostic(off, chromium.subgroup_matrix_uniformity);
15
 
16
+
17
  {{ env.wgsl.resourceDeclarations }}
18
 
19
  const M: u32 = {{ M }}u;
 
65
  {% else %}
66
  let zero = {{ defaultZero }};
67
  {% endif %}
68
+ // The blob is packed four bytes per u32 word. The slice starts on an 8-code
69
+ // boundary inside a 4-byte-aligned blob, so every packed word below holds
70
+ // 32 / bits consecutive codes of this column and is read once.
71
+ let blob_base = (n * KBLOCKS + block) * BLOB_SIZE;
72
  {% if bits == 2 %}
73
+ // Sixteen 2-bit codes per word; the slice may start mid-word.
 
74
  for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
75
  let code_offset = offset0 + i;
76
+ let byte_index = blob_base + (code_offset >> 2u);
77
+ let shift = (byte_index & 3u) * 8u + (code_offset & 3u) * 2u;
78
+ tile_B[row * TILE_K + col + i] = (f32((b[byte_index >> 2u] >> shift) & 3u) - zero) * scale;
79
  }
80
  {% elif bits == 4 %}
81
+ for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 8u) {
82
+ let packed = b[(blob_base + ((offset0 + i) >> 1u)) >> 2u];
83
+ {% for h in range(8) %}
84
+ tile_B[row * TILE_K + col + i + {{ h }}u] = (f32((packed >> {{ h * 4 }}u) & 15u) - zero) * scale;
85
+ {% endfor %}
86
  }
87
  {% else %}
88
+ for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 4u) {
89
+ let packed = b[(blob_base + offset0 + i) >> 2u];
90
+ {% for h in range(4) %}
91
+ tile_B[row * TILE_K + col + i + {{ h }}u] = (f32((packed >> {{ h * 8 }}u) & 255u) - zero) * scale;
92
+ {% endfor %}
93
  }
94
  {% endif %}
95
  } else {
 
150
  workgroupBarrier();
151
 
152
  for (var step = 0u; step < TILE_K; step = step + 8u) {
153
+ {% set operandScalar = "f32" %}{% set directInputs = directMatrixInputs is defined and directMatrixInputs %}
154
+ let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
155
+ {% for r in range(2) %}
156
+ var matA{{ r }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ "w" if directInputs else "tile_A" }}, matrix_a_offset{% if r > 0 %} + 8u * {{ "K" if directInputs else "TILE_K" }}{% endif %}, {{ "K" if directInputs else "TILE_K" }});
157
+ {% endfor %}
158
 
159
  let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
160
+ {% for c in range(4) %}
161
+ var matB{{ c }}: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&{{ "xm" if directInputs else "tile_B" }}, matrix_b_offset{% if c > 0 %} + {{ c * 8 }}u * TILE_K{% endif %}, {{ "N" if directInputs else "TILE_K" }});
162
+ {% endfor %}
 
163
 
164
  matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
165
  matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
build/webgpu/matmul-nbits.wgsl.jinja CHANGED
@@ -1,6 +1,3 @@
1
- {% if usesF16 %}
2
- enable f16;
3
- {% endif %}
4
  {{ env.wgsl.resourceDeclarations }}
5
 
6
  const WG: u32 = {{ workgroupSize }}u;
@@ -12,29 +9,27 @@ const WG: u32 = {{ workgroupSize }}u;
12
  {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
13
  fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
14
  {% if bits == 2 %}
15
- let byte_index = offset / 4u;
16
- let shift = (offset % 4u) * 2u;
17
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
18
- return ({{ buffer }}[packed_index] >> shift) & 3u;
19
  {% elif bits == 4 %}
20
- let byte_index = offset / 2u;
21
- let shift = (offset % 2u) * 4u;
22
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + byte_index;
23
- return ({{ buffer }}[packed_index] >> shift) & 15u;
24
  {% else %}
25
- let packed_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
26
- return {{ buffer }}[packed_index] & 255u;
27
  {% endif %}
28
  }
29
  {%- endmacro %}
30
  {{- matmul_nbits_packed_code(bits=bits) }}
31
 
32
  @compute @workgroup_size(WG, 1, 1)
33
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
34
  // 2D-folded flat output-element index: gid.y carries the high bits past the
35
- // maxComputeWorkgroupsPerDimension dispatch limit. When nwg.y == 1 this
36
  // reduces to gid.x; the index >= total guard drops the tail.
37
- let index = gid.x + gid.y * nwg.x * WG;
38
  let total = params.rows * params.N;
39
 
40
  if (index >= total) {
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  const WG: u32 = {{ workgroupSize }}u;
 
9
  {% macro matmul_nbits_packed_code(fn="packed_weight", buffer="b", kBlocks="params.kBlocks", blobSize="params.blobSize", bits=4) %}
10
  fn {{ fn }}(n: u32, block: u32, offset: u32) -> u32 {
11
  {% if bits == 2 %}
12
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 2u);
13
+ let shift = (byte_index & 3u) * 8u + (offset & 3u) * 2u;
14
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 3u;
 
15
  {% elif bits == 4 %}
16
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + (offset >> 1u);
17
+ let shift = (byte_index & 3u) * 8u + (offset & 1u) * 4u;
18
+ return ({{ buffer }}[byte_index >> 2u] >> shift) & 15u;
 
19
  {% else %}
20
+ let byte_index = (n * {{ kBlocks }} + block) * {{ blobSize }} + offset;
21
+ return ({{ buffer }}[byte_index >> 2u] >> ((byte_index & 3u) * 8u)) & 255u;
22
  {% endif %}
23
  }
24
  {%- endmacro %}
25
  {{- matmul_nbits_packed_code(bits=bits) }}
26
 
27
  @compute @workgroup_size(WG, 1, 1)
28
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
29
  // 2D-folded flat output-element index: gid.y carries the high bits past the
30
+ // per-axis dispatch fold width. With no fold this
31
  // reduces to gid.x; the index >= total guard drops the tail.
32
+ let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
33
  let total = params.rows * params.N;
34
 
35
  if (index >= total) {
build/webgpu/metadata.json CHANGED
@@ -1,25 +1,63 @@
1
  {
2
  "name": "com.microsoft.MatMulNBits",
3
- "id": "_com_microsoft_matmulnbits_webgpu_5495bd2",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
- "bench.json": "BbpRtHBH/vkJDaybyRBNKhuUCpe4XMJtKmW3bhRcEZE=",
11
- "manifest.json": "z8oCYWRaMHew5ogYuf7Txv4/6y2qpVoVODDlLxEZ+vA=",
12
- "matmul-nbits-dp4a-quantize.wgsl.jinja": "WEUGSDH5qWYg10f/mkAHxq8AkIkMW9R/Wn/Y23Q0Jw0=",
13
- "matmul-nbits-gemv-q4.wgsl.jinja": "x1LxVx2cJBdN2cqapXz2T6AScpTcbmTO+B3tzvIXuT8=",
14
- "matmul-nbits-q4-dp4a-prefill.wgsl.jinja": "qUrURZDpPzMpABnTgESkHgLCQiAWxEZ8wvzkAPTW2kI=",
15
- "matmul-nbits-q4-prefill-tile4x4.wgsl.jinja": "HjZraSjOpaSl5Cjns4scselOIn+ibxFEu+9LTeUI0QI=",
16
- "matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja": "oU7BcxJnKIEfgnkWHPq77RrCmrE0iIm6ED7cgVfZUYs=",
17
- "matmul-nbits-q4-prefill-tiled.wgsl.jinja": "TdetCiv5o88hvWCGNDewpFX+M+NZrz9rekU/d5kMZTM=",
18
- "matmul-nbits-q4-sgmat.wgsl.jinja": "PfO9QrUyNflp7RtUbUTRDwx7GDI6UUwz9I+6mJpO1OA=",
19
- "matmul-nbits.wgsl.jinja": "DRkwODEER11pYQxt2ssS9YvFggO3CbUqQ9yag53J5lY=",
20
- "test.json": "XLwnbLdOJASke2yBnDScjJi+o2XISmdRCebb2jwWpDI="
 
21
  }
22
  },
23
- "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
24
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.MatMulNBits" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
1
  {
2
  "name": "com.microsoft.MatMulNBits",
3
+ "id": "_com_microsoft_matmulnbits_webgpu_6f18c00",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "P8wNyT1cbaSLn1VS4hVRFQxLYwji4Z7gp69Mt2pQaFo=",
11
+ "manifest.json": "QPPdzEGo1lJtp16tQaaqA9UHKPlvBY3fq66gG4XPvyQ=",
12
+ "matmul-nbits-dp4a-quantize.wgsl.jinja": "0gqEvgBRzH2Demz/RvrFkUOV487GyCq7Ujd26GyD+xw=",
13
+ "matmul-nbits-gemv-q4.wgsl.jinja": "ewlLPcW7t3UdnoLymZBoXnV+1oOeB9+YTyaOAnPhhfA=",
14
+ "matmul-nbits-q4-dp4a-prefill.wgsl.jinja": "pd9dWWUdIgYFVCRayU5OlvjYIvyqPvkOGgwtEPNMHas=",
15
+ "matmul-nbits-q4-prefill-tile4x4.wgsl.jinja": "8DVy3szxcxwVItIlEXoioQi2YxDmmd5TCFW4BCYPiCQ=",
16
+ "matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja": "W6JEeRleux7Jm3wmaflEusYFNuCiFtXnBTEW5cfS4Fk=",
17
+ "matmul-nbits-q4-prefill-tiled.wgsl.jinja": "eAEIbpkW0qziqyXhYNRiVbCuDRghjoz8EjnOrP8AgD8=",
18
+ "matmul-nbits-q4-sgmat.wgsl.jinja": "SFEMothi+irkTIclMjeTF1sCmB61Ai/2hGgnoOyNMUM=",
19
+ "matmul-nbits.wgsl.jinja": "UndxgqiV/O19Plpxda1d388lnQWyeCugbBvJGcYr9t8=",
20
+ "reduce-axis0-splitk-combine.wgsl.jinja": "Yz1hjK55R/kndUrw3ugPqgPaOBwKmYupqZoVdJTHO5Q=",
21
+ "test.json": "Hnb4z+ExdwYZb4EjSBR0gdGi5eY1kh7DaoGj3UTC5Ug="
22
  }
23
  },
24
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
25
+ "webgpu": {
26
+ "manifestSpec": "2.0",
27
+ "variants": {
28
+ "q4_dp4a_prefill": ["matmul-nbits-dp4a-quantize.wgsl.jinja", "matmul-nbits-q4-dp4a-prefill.wgsl.jinja"],
29
+ "gemv_default_zero": ["matmul-nbits-gemv-q4.wgsl.jinja"],
30
+ "prefill_sgmat_default_zero": ["matmul-nbits-q4-sgmat.wgsl.jinja"],
31
+ "prefill_tiled_reg_vec4_splitk_default_zero": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja", "reduce-axis0-splitk-combine.wgsl.jinja"],
32
+ "prefill_tiled_reg_vec4_default_zero": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
33
+ "prefill_tiled_reg_default_zero": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
34
+ "prefill_tiled_default_zero": ["matmul-nbits-q4-prefill-tiled.wgsl.jinja"],
35
+ "prefill_tile4x4_default_zero": ["matmul-nbits-q4-prefill-tile4x4.wgsl.jinja"],
36
+ "default_zero": ["matmul-nbits.wgsl.jinja"],
37
+ "gemv_zero_bias": ["matmul-nbits-gemv-q4.wgsl.jinja"],
38
+ "prefill_sgmat_zero_bias": ["matmul-nbits-q4-sgmat.wgsl.jinja"],
39
+ "prefill_tiled_reg_vec4_splitk_zero_bias": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja", "reduce-axis0-splitk-combine.wgsl.jinja"],
40
+ "prefill_tiled_reg_vec4_zero_bias": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
41
+ "prefill_tiled_reg_zero_bias": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
42
+ "prefill_tiled_zero_bias": ["matmul-nbits-q4-prefill-tiled.wgsl.jinja"],
43
+ "prefill_tile4x4_zero_bias": ["matmul-nbits-q4-prefill-tile4x4.wgsl.jinja"],
44
+ "zero_bias": ["matmul-nbits.wgsl.jinja"],
45
+ "gemv_zero_only": ["matmul-nbits-gemv-q4.wgsl.jinja"],
46
+ "prefill_sgmat_zero_only": ["matmul-nbits-q4-sgmat.wgsl.jinja"],
47
+ "prefill_tiled_reg_vec4_splitk_zero_only": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja", "reduce-axis0-splitk-combine.wgsl.jinja"],
48
+ "prefill_tiled_reg_vec4_zero_only": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
49
+ "prefill_tiled_reg_zero_only": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
50
+ "prefill_tiled_zero_only": ["matmul-nbits-q4-prefill-tiled.wgsl.jinja"],
51
+ "prefill_tile4x4_zero_only": ["matmul-nbits-q4-prefill-tile4x4.wgsl.jinja"],
52
+ "zero_only": ["matmul-nbits.wgsl.jinja"],
53
+ "gemv_bias_only": ["matmul-nbits-gemv-q4.wgsl.jinja"],
54
+ "prefill_sgmat_bias_only": ["matmul-nbits-q4-sgmat.wgsl.jinja"],
55
+ "prefill_tiled_reg_vec4_splitk_bias_only": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja", "reduce-axis0-splitk-combine.wgsl.jinja"],
56
+ "prefill_tiled_reg_vec4_bias_only": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
57
+ "prefill_tiled_reg_bias_only": ["matmul-nbits-q4-prefill-tiled-reg.wgsl.jinja"],
58
+ "prefill_tiled_bias_only": ["matmul-nbits-q4-prefill-tiled.wgsl.jinja"],
59
+ "prefill_tile4x4_bias_only": ["matmul-nbits-q4-prefill-tile4x4.wgsl.jinja"],
60
+ "bias_only": ["matmul-nbits.wgsl.jinja"]
61
+ }
62
+ }
63
  }
build/webgpu/reduce-axis0-splitk-combine.wgsl.jinja ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 2 of the split-K column-wise reduction. One invocation per output column
2
+ // folds the segment partials and applies the selected reduction's final step.
3
+ // Segments are folded in ascending order for deterministic results. This order
4
+ // differs from the single-pass reduction but remains within the f32 tolerance.
5
+ {% set addBias = addBias is defined and addBias %}
6
+ {% set biasCols = biasCols | default(0) %}
7
+ {% set intMode = intMode is defined and intMode %}
8
+ {% set yv = "f16(" if outputF16 else "" %}
9
+ {% set vy = ")" if outputF16 else "" %}
10
+ {% if outputF16 %}
11
+ enable f16;
12
+ {% endif %}
13
+ {{ env.wgsl.resourceDeclarations }}
14
+ {% macro wgsl_minmax_identity(name, op, scalar="f32") %}
15
+ /* Exact {{ op }} reduction identity. WGSL rejects infinity during constant
16
+ * evaluation, so an f32 identity is constructed from its IEEE-754 bits. */
17
+ {% set logicalBoolIdentity = logicalBool is defined and logicalBool %}
18
+ fn {{ name }}() -> {{ scalar }} {
19
+ {% if scalar == "i32" %}
20
+ return {{ "-2147483647i - 1i" if op == "max" else "2147483647i" }};
21
+ {% elif scalar == "u32" %}
22
+ return {{ "1u" if logicalBoolIdentity and op == "min" else "0u" if op == "max" else "4294967295u" }};
23
+ {% else %}
24
+ var bits = {{ "0xff800000u" if op == "max" else "0x7f800000u" }};
25
+ return bitcast<f32>(bits);
26
+ {% endif %}
27
+ }
28
+ {%- endmacro %}
29
+
30
+
31
+ const WG: u32 = {{ workgroupSize }}u;
32
+ const SPLIT: u32 = {{ split }}u;
33
+ {% if addBias %}
34
+ const BIAS_COLS: u32 = {{ biasCols }}u;
35
+ {% endif %}
36
+ {% if op == "logsumexp" %}
37
+ const F32_MIN: f32 = -3.4028234663852886e38;
38
+ const F32_MAX: f32 = 3.4028234663852886e38;
39
+
40
+ fn is_nan_f32(value: f32) -> bool {
41
+ let bits = bitcast<u32>(value);
42
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
43
+ }
44
+ {% elif op == "max" or op == "min" %}
45
+ {{ wgsl_minmax_identity("reduction_identity", op) }}
46
+ {% endif %}
47
+
48
+ @compute @workgroup_size(WG, 1, 1)
49
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
50
+ @builtin(num_workgroups) nwg: vec3<u32>) {
51
+ let stride = nwg.x * WG;
52
+ let start = (gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG) + gid.x;
53
+ for (var col = start; col < params.cols; col = col + stride) {
54
+ {% if op == "logsumexp" %}
55
+ // Merge SPLIT (segMax, segSumExp) pairs stably; carry NaN / +Inf markers.
56
+ var nan_value = 0.0;
57
+ var has_nan = false;
58
+ var global_max = F32_MIN;
59
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
60
+ let nv = partials[(2u * SPLIT + seg) * params.cols + col];
61
+ if (nv != 0.0 || is_nan_f32(nv)) {
62
+ has_nan = true;
63
+ nan_value = nv;
64
+ }
65
+ global_max = max(global_max, partials[seg * params.cols + col]);
66
+ }
67
+ var sum = 0.0;
68
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
69
+ let seg_max = partials[seg * params.cols + col];
70
+ let seg_sum = partials[(SPLIT + seg) * params.cols + col];
71
+ sum = sum + seg_sum * exp(seg_max - global_max);
72
+ }
73
+ let has_positive_inf = global_max > F32_MAX;
74
+ let finite_or_inf = select(global_max + log(sum), global_max, has_positive_inf);
75
+ y[col] = {{ yv }}select(finite_or_inf, nan_value, has_nan){{ vy }};
76
+ {% else %}
77
+ {% if intMode %}
78
+ {% if op == "prod" %}
79
+ var total = 1i;
80
+ {% else %}
81
+ var total = 0i;
82
+ {% endif %}
83
+ {% else %}
84
+ {% if op == "max" %}
85
+ var total = reduction_identity();
86
+ {% elif op == "min" %}
87
+ var total = reduction_identity();
88
+ {% elif op == "prod" %}
89
+ var total = 1.0;
90
+ {% else %}
91
+ var total = 0.0;
92
+ {% endif %}
93
+ {% endif %}
94
+ for (var seg = 0u; seg < SPLIT; seg = seg + 1u) {
95
+ let p = partials[seg * params.cols + col];
96
+ {% if op == "max" or op == "min" %}
97
+ total = {{ op }}(total, p);
98
+ {% elif op == "prod" %}
99
+ total = total * p;
100
+ {% else %}
101
+ total = total + p;
102
+ {% endif %}
103
+ }
104
+ {% if addBias %}
105
+ total = total + f32(bias[col % BIAS_COLS]);
106
+ {% endif %}
107
+ {% if op == "l2" %}
108
+ y[col] = {{ yv }}sqrt(total){{ vy }};
109
+ {% elif op == "logsum" %}
110
+ y[col] = {{ yv }}log(total){{ vy }};
111
+ {% elif op == "mean" %}
112
+ y[col] = {{ yv }}total / f32(params.rows){{ vy }};
113
+ {% else %}
114
+ {% if outputF16 %}
115
+ y[col] = f16(total);
116
+ {% else %}
117
+ y[col] = total;
118
+ {% endif %}
119
+ {% endif %}
120
+ {% endif %}
121
+ }
122
+ }
build/webgpu/test.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.MatMulNBits",
3
  "fixtureArrays": {
4
  "q4_prefill_tiled_reg_tailk_m32_k33_n4096_input_bT": [16, 50, 84, 118, 152, 186, 220, 254, 135, 119, 102, 85, 68, 51, 34, 17],
5
  "q4_weight_cycle_b_t": [16, 50, 84, 118, 152, 186, 220, 254, 33, 67, 101, 135, 169, 203, 237, 15],
@@ -78,7 +77,7 @@
78
  {
79
  "name": "q8_zero_bias_prefill_tile4x4_small_m8",
80
  "provenance": {
81
- "notes": "q8 companion to q4_zero_bias_prefill_tile4x4_small_m8: the relaxed tile4x4 bias floor (M>=2) also makes prefill_tile4x4_zero_bias selectable for q8 prefill+bias at M in [2,64). Validates the q8 unpack branch + bias epilogue on the tile4x4 path at small M."
82
  },
83
  "inputs": {
84
  "aT": {
@@ -181,7 +180,7 @@
181
  "provenance": {
182
  "source": "onnxruntime/test/contrib_ops/matmul_4bits_test.cc",
183
  "test": "MatMulNBits.UnsupportedBlockSize_512",
184
- "notes": "The public schema permits every power-of-two block size of at least 16, and this package implements block_size=512. ORT's installed, pinned, and inspected current CPU providers instead restrict execution to 16, 32, 64, 128, or 256. With A=1, packed q4 codes=9, default zero point=8, and scale=0.25, the exact result is 512 * (9 - 8) * 0.25 = 128."
185
  },
186
  "inputs": {
187
  "aT": { "dtype": "float32", "shape": [1, 512], "data": { "kind": "constant", "value": 1.0 } },
@@ -242,7 +241,9 @@
242
  },
243
  {
244
  "name": "q4_zero_bias_prefill_sgmat_m32_storage_floor",
245
- "provenance": { "notes": "Zero-point/bias sibling of the 32-row subgroup-matrix storage-floor regression." },
 
 
246
  "inputs": {
247
  "aT": {
248
  "dtype": "float32",
@@ -276,7 +277,7 @@
276
  {
277
  "name": "q4_no_zero_prefill_g64_sgmat",
278
  "provenance": {
279
- "notes": "Compact blockSize=64 q4 default-zero prefill sibling for model g64 benchmarks. Full tier routes through prefill_sgmat_default_zero; no-MMA/no-subgroups/baseline route through prefill_tiled_default_zero."
280
  },
281
  "inputs": {
282
  "aT": {
@@ -323,7 +324,7 @@
323
  {
324
  "name": "q8_no_zero_prefill_sgmat_m32_storage_floor",
325
  "provenance": {
326
- "notes": "q8 sibling proving the 32-row subgroup-matrix tile remains available at the 16 KiB storage floor."
327
  },
328
  "inputs": {
329
  "aT": {
@@ -377,9 +378,131 @@
377
  "outputs": { "yT": { "dtype": "float32", "shape": [128, 64], "tolerance": 0.001 } },
378
  "attrs": { "K": 128, "N": 64, "bits": 8, "block_size": 32 }
379
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
  {
381
  "name": "q8_zero_bias_prefill_sgmat_m32_storage_floor",
382
- "provenance": { "notes": "q8 zero-point/bias sibling of the 32-row subgroup-matrix storage-floor regression." },
 
 
383
  "inputs": {
384
  "aT": {
385
  "dtype": "float32",
@@ -692,9 +815,7 @@
692
  }
693
  },
694
  "outputs": { "yT": { "dtype": "float16", "shape": [512, 1024], "tolerance": 0.0001, "relTolerance": 0.001 } },
695
- "provenance": {
696
- "notes": "f16 activations on the register-tiled prefill path: this is the only shape class where the shared A/B tiles are staged in f16, so it is the fixture that covers that staging."
697
- },
698
  "attrs": { "K": 128, "N": 1024, "bits": 4, "block_size": 32 }
699
  },
700
  {
@@ -772,7 +893,7 @@
772
  },
773
  "outputs": { "yT": { "dtype": "float16", "shape": [512, 1024], "tolerance": 0.0001, "relTolerance": 0.001 } },
774
  "provenance": {
775
- "notes": "q8 sibling of q4_f16_prefill_reg_m512_n1024. 8-bit codes give the widest dequantized range, so the f16 tile store rounds hardest here."
776
  },
777
  "attrs": { "K": 128, "N": 1024, "bits": 8, "block_size": 32 }
778
  },
@@ -836,7 +957,7 @@
836
  {
837
  "name": "q4_gemv_default_zero_m1_tail_n7",
838
  "provenance": {
839
- "notes": "M=1 GEMV with N=7 (not a multiple of 4). Exercises the vec4 tail-column store guards (col_base+1/2/3 < N) in matmul-nbits-gemv-q4.wgsl.jinja; existing M==1 tests all use N=4. Routes to gemv_default_zero (prio 20)."
840
  },
841
  "inputs": {
842
  "aT": {
@@ -861,7 +982,7 @@
861
  {
862
  "name": "q4_gemv_default_zero_m1_odd_k33_n5",
863
  "provenance": {
864
- "notes": "K=33 is odd, so the last stored byte of the second block carries one live nibble and one past the end of K. The GEMV walks whole bytes, so this is the only shape that exercises its out-of-range activation guard; every other M==1 case has an even K."
865
  },
866
  "inputs": {
867
  "aT": {
@@ -886,7 +1007,7 @@
886
  {
887
  "name": "q2_gemv_default_zero_m1_k18_n5",
888
  "provenance": {
889
- "notes": "The only two-bit M==1 case: four codes share one stored byte, and K=18 leaves two of the last byte's four codes past the end of K. Covers both the two-bit unpack in the GEMV and its out-of-range activation guard at the widest code-per-byte packing."
890
  },
891
  "inputs": {
892
  "aT": {
@@ -938,7 +1059,7 @@
938
  "name": "q4_gemv_default_zero_m1_tail_n7_ncols8",
939
  "tunables": { "GEMV_N_COLS": 8 },
940
  "provenance": {
941
- "notes": "GEMV_N_COLS=8 twin of tail_n7: one workgroup, second column group entirely tail-guarded (cols 4-6 active, 7 out of range)."
942
  },
943
  "inputs": {
944
  "aT": {
@@ -963,7 +1084,7 @@
963
  {
964
  "name": "q8_zero_bias_gemv_m1_tail_n5",
965
  "provenance": {
966
- "notes": "M=1 q8 GEMV with N=5 (not a multiple of 4), WITH zero_points and bias. Exercises the vec4 tail-column store guards together with the q8 unpack and bias epilogue in matmul-nbits-gemv-q4.wgsl.jinja. Routes to gemv_zero_bias (prio 20)."
967
  },
968
  "inputs": {
969
  "aT": {
@@ -1000,7 +1121,7 @@
1000
  "provenance": {
1001
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1002
  "test": "independent has_zero_points/has_bias epilogues",
1003
- "notes": "M=1 q8 GEMV with explicit zero points and no bias. This pins the zero-points-only binding layout and epilogue instead of relying on the combined zero-point-plus-bias case."
1004
  },
1005
  "inputs": {
1006
  "aT": {
@@ -1032,7 +1153,7 @@
1032
  "provenance": {
1033
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1034
  "test": "independent has_zero_points/has_bias epilogues",
1035
- "notes": "M=1 q8 GEMV with bias and the schema-default zero point. This pins the bias-only binding layout and epilogue instead of relying on the combined zero-point-plus-bias case."
1036
  },
1037
  "inputs": {
1038
  "aT": {
@@ -1063,7 +1184,7 @@
1063
  "name": "q8_zero_bias_gemv_m1_tail_n5_ncols8",
1064
  "tunables": { "GEMV_N_COLS": 8 },
1065
  "provenance": {
1066
- "notes": "GEMV_N_COLS=8 twin of tail_n5: q8 unpack + zero_points + bias epilogue with only one live column in the second group."
1067
  },
1068
  "inputs": {
1069
  "aT": {
@@ -1098,7 +1219,7 @@
1098
  {
1099
  "name": "q8_zero_bias_naive_fallback_tailK_m3_n6",
1100
  "provenance": {
1101
- "notes": "K=17,blockSize=16 gives a tail block; N=6 is below the register-tiled floor and the aligned tile paths are ineligible, while M=3 bypasses GEMV. With zero_points+bias present this routes to the priority-0 zero_bias scalar fallback and covers q8 unpack, per-block zero points, bias, and a partial K block."
1102
  },
1103
  "inputs": {
1104
  "aT": {
@@ -1161,8 +1282,8 @@
1161
  {
1162
  "name": "q4_prefill_tiled_reg_aligned_m32_k32_n4096_at_workgroup_floor",
1163
  "provenance": {
1164
- "source": "authored for route coverage",
1165
- "notes": "Aligned-K twin of the tail-K case at the same M and N. A tail-K shape skips the register tile's workgroup floor entirely, so no fixture exercised that floor; this one sits exactly on it at 64 workgroups."
1166
  },
1167
  "inputs": {
1168
  "aT": {
@@ -1190,7 +1311,7 @@
1190
  {
1191
  "name": "q4_prefill_tiled_reg_tailk_m32_k33_n512_below_wide_n_floor",
1192
  "provenance": {
1193
- "notes": "Tail-block coverage below the ordinary wide register-tile N floor. K=33 exercises the padded final block, while N=512 keeps the case inside the relaxed band that the larger N=4096 case cannot distinguish."
1194
  },
1195
  "inputs": {
1196
  "aT": {
@@ -1273,7 +1394,7 @@
1273
  "provenance": {
1274
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1275
  "test": "independent has_zero_points/has_bias epilogues",
1276
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1277
  },
1278
  "inputs": {
1279
  "aT": {
@@ -1305,7 +1426,7 @@
1305
  "provenance": {
1306
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1307
  "test": "independent has_zero_points/has_bias epilogues",
1308
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1309
  },
1310
  "inputs": {
1311
  "aT": {
@@ -1360,7 +1481,7 @@
1360
  "provenance": {
1361
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1362
  "test": "independent has_zero_points/has_bias epilogues",
1363
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1364
  },
1365
  "attrs": { "K": 128, "N": 64, "bits": 8, "block_size": 32 }
1366
  },
@@ -1392,7 +1513,7 @@
1392
  "provenance": {
1393
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1394
  "test": "independent has_zero_points/has_bias epilogues",
1395
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1396
  },
1397
  "attrs": { "K": 128, "N": 64, "bits": 8, "block_size": 32 }
1398
  },
@@ -1424,7 +1545,7 @@
1424
  "provenance": {
1425
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1426
  "test": "independent has_zero_points/has_bias epilogues",
1427
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1428
  },
1429
  "attrs": { "K": 128, "N": 1024, "bits": 4, "block_size": 32 }
1430
  },
@@ -1456,7 +1577,7 @@
1456
  "provenance": {
1457
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1458
  "test": "independent has_zero_points/has_bias epilogues",
1459
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1460
  },
1461
  "attrs": { "K": 128, "N": 1024, "bits": 4, "block_size": 32 }
1462
  },
@@ -1488,7 +1609,7 @@
1488
  "provenance": {
1489
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1490
  "test": "independent has_zero_points/has_bias epilogues",
1491
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1492
  },
1493
  "attrs": { "K": 128, "N": 1024, "bits": 8, "block_size": 32 }
1494
  },
@@ -1520,7 +1641,7 @@
1520
  "provenance": {
1521
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1522
  "test": "independent has_zero_points/has_bias epilogues",
1523
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1524
  },
1525
  "attrs": { "K": 128, "N": 1024, "bits": 8, "block_size": 32 }
1526
  },
@@ -1529,7 +1650,7 @@
1529
  "provenance": {
1530
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1531
  "test": "independent has_zero_points/has_bias epilogues",
1532
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1533
  },
1534
  "inputs": {
1535
  "aT": {
@@ -1561,7 +1682,7 @@
1561
  "provenance": {
1562
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1563
  "test": "independent has_zero_points/has_bias epilogues",
1564
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1565
  },
1566
  "inputs": {
1567
  "aT": {
@@ -1593,7 +1714,7 @@
1593
  "provenance": {
1594
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1595
  "test": "independent has_zero_points/has_bias epilogues",
1596
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1597
  },
1598
  "inputs": {
1599
  "aT": {
@@ -1625,7 +1746,7 @@
1625
  "provenance": {
1626
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1627
  "test": "independent has_zero_points/has_bias epilogues",
1628
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1629
  },
1630
  "inputs": {
1631
  "aT": {
@@ -1680,7 +1801,7 @@
1680
  "provenance": {
1681
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1682
  "test": "independent has_zero_points/has_bias epilogues",
1683
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1684
  },
1685
  "attrs": { "K": 128, "N": 64, "bits": 4, "block_size": 32 }
1686
  },
@@ -1712,7 +1833,7 @@
1712
  "provenance": {
1713
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1714
  "test": "independent has_zero_points/has_bias epilogues",
1715
- "notes": "zero_points-only and bias-only inputs must stay on the fast kernels instead of falling to the naive per-element path."
1716
  },
1717
  "attrs": { "K": 128, "N": 64, "bits": 4, "block_size": 32 }
1718
  },
@@ -1738,7 +1859,7 @@
1738
  "provenance": {
1739
  "source": "onnxruntime/contrib_ops/webgpu/quantization/dp4a_matmul_nbits.cc",
1740
  "test": "accuracy_level=4 int8 activation quantization (DP4AMatMulQuantize + DP4AMatMulNBits)",
1741
- "notes": "accuracy_level 4 permits int8 activation math: A is quantized per 128-element block (scale = max_abs / 127, values pack4x8snorm-rounded) and multiplied against the 4-bit weights with packed integer dots. The reference stays float, so tolerances absorb the permitted quantization error."
1742
  },
1743
  "attrs": { "K": 256, "N": 64, "bits": 4, "block_size": 32, "accuracy_level": 4 }
1744
  },
@@ -1753,7 +1874,7 @@
1753
  "bT": {
1754
  "dtype": "uint8",
1755
  "shape": [16, 2, 32],
1756
- "data": { "kind": "cycle", "values": [37, 201, 116, 88, 250, 13, 164, 95, 142, 7, 233] }
1757
  },
1758
  "scalesT": { "dtype": "float32", "shape": [16, 2], "data": { "kind": "linspace", "start": 0.02, "end": 0.06 } }
1759
  },
@@ -1761,7 +1882,7 @@
1761
  "provenance": {
1762
  "source": "onnxruntime/contrib_ops/webgpu/quantization/dp4a_matmul_nbits.cc",
1763
  "test": "accuracy_level=4 int8 activation quantization (DP4AMatMulQuantize + DP4AMatMulNBits)",
1764
- "notes": "accuracy_level 4 permits int8 activation math: A is quantized per 128-element block (scale = max_abs / 127, values pack4x8snorm-rounded) and multiplied against the 4-bit weights with packed integer dots. The reference stays float, so tolerances absorb the permitted quantization error."
1765
  },
1766
  "attrs": { "K": 128, "N": 16, "bits": 4, "block_size": 64, "accuracy_level": 4 }
1767
  },
@@ -2012,6 +2133,151 @@
2012
  },
2013
  "outputs": { "yT": { "dtype": "float32", "shape": [3, 6], "tolerance": 0.001 } },
2014
  "attrs": { "K": 17, "N": 6, "bits": 8, "block_size": 16 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2015
  }
2016
  ]
2017
  }
 
1
  {
 
2
  "fixtureArrays": {
3
  "q4_prefill_tiled_reg_tailk_m32_k33_n4096_input_bT": [16, 50, 84, 118, 152, 186, 220, 254, 135, 119, 102, 85, 68, 51, 34, 17],
4
  "q4_weight_cycle_b_t": [16, 50, 84, 118, 152, 186, 220, 254, 33, 67, 101, 135, 169, 203, 237, 15],
 
77
  {
78
  "name": "q8_zero_bias_prefill_tile4x4_small_m8",
79
  "provenance": {
80
+ "notes": "An eight-row q8 prefill with zero points and bias exercises q8 unpacking and the bias epilogue on the small-M tile4x4 route."
81
  },
82
  "inputs": {
83
  "aT": {
 
180
  "provenance": {
181
  "source": "onnxruntime/test/contrib_ops/matmul_4bits_test.cc",
182
  "test": "MatMulNBits.UnsupportedBlockSize_512",
183
+ "notes": "The schema permits power-of-two block sizes of at least 16, including 512. With A=1, q4 code 9, default zero point 8, and scale 0.25, the result is exactly `512 * (9 - 8) * 0.25 = 128`."
184
  },
185
  "inputs": {
186
  "aT": { "dtype": "float32", "shape": [1, 512], "data": { "kind": "constant", "value": 1.0 } },
 
241
  },
242
  {
243
  "name": "q4_zero_bias_prefill_sgmat_m32_storage_floor",
244
+ "provenance": {
245
+ "notes": "Explicit zero points and bias exercise the 32-row subgroup-matrix tile at the 16 KiB workgroup-storage floor."
246
+ },
247
  "inputs": {
248
  "aT": {
249
  "dtype": "float32",
 
277
  {
278
  "name": "q4_no_zero_prefill_g64_sgmat",
279
  "provenance": {
280
+ "notes": "A compact q4 prefill with block size 64 and the default zero point exercises the subgroup-matrix route and its portable tiled fallback."
281
  },
282
  "inputs": {
283
  "aT": {
 
324
  {
325
  "name": "q8_no_zero_prefill_sgmat_m32_storage_floor",
326
  "provenance": {
327
+ "notes": "A q8 prefill exercises the 32-row subgroup-matrix tile at the 16 KiB workgroup-storage floor."
328
  },
329
  "inputs": {
330
  "aT": {
 
378
  "outputs": { "yT": { "dtype": "float32", "shape": [128, 64], "tolerance": 0.001 } },
379
  "attrs": { "K": 128, "N": 64, "bits": 8, "block_size": 32 }
380
  },
381
+ {
382
+ "name": "q8_no_zero_prefill_reg_vec4_splitk_m128_k1024_n1024",
383
+ "provenance": {
384
+ "notes": "Without zero points or bias, 32 output tiles over K=1024 use two K slices whose float32 partial sums are combined."
385
+ },
386
+ "inputs": {
387
+ "aT": {
388
+ "dtype": "float32",
389
+ "shape": [128, 1024],
390
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 0.35 }
391
+ },
392
+ "bT": {
393
+ "dtype": "uint8",
394
+ "shape": [1024, 32, 32],
395
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/mixed_weight_cycle_b_t" } }
396
+ },
397
+ "scalesT": {
398
+ "dtype": "float32",
399
+ "shape": [1024, 32],
400
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
401
+ }
402
+ },
403
+ "outputs": { "yT": { "dtype": "float32", "shape": [128, 1024], "tolerance": 0.005 } },
404
+ "attrs": { "K": 1024, "N": 1024, "bits": 8, "block_size": 32 }
405
+ },
406
+ {
407
+ "name": "q8_zero_bias_prefill_reg_vec4_splitk_m128_k1024_n1024",
408
+ "provenance": {
409
+ "notes": "With zero points and bias, the split-K four-wide route applies zero points in each partial pass and adds bias once in the combine."
410
+ },
411
+ "inputs": {
412
+ "aT": {
413
+ "dtype": "float32",
414
+ "shape": [128, 1024],
415
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 0.35 }
416
+ },
417
+ "bT": {
418
+ "dtype": "uint8",
419
+ "shape": [1024, 32, 32],
420
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/mixed_weight_cycle_b_t" } }
421
+ },
422
+ "scalesT": {
423
+ "dtype": "float32",
424
+ "shape": [1024, 32],
425
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
426
+ },
427
+ "zeroPointsT": {
428
+ "dtype": "float32",
429
+ "shape": [1024, 32],
430
+ "data": { "kind": "cycle", "values": [120.0, 132.0, 128.0, 140.0, 135.0, 118.0, 144.0, 126.0] }
431
+ },
432
+ "biasT": {
433
+ "dtype": "float32",
434
+ "shape": [1024],
435
+ "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
436
+ }
437
+ },
438
+ "outputs": { "yT": { "dtype": "float32", "shape": [128, 1024], "tolerance": 0.005 } },
439
+ "attrs": { "K": 1024, "N": 1024, "bits": 8, "block_size": 32 }
440
+ },
441
+ {
442
+ "name": "q8_zero_only_prefill_reg_vec4_splitk_m128_k1024_n1024",
443
+ "provenance": {
444
+ "notes": "With zero points and no bias, the split-K four-wide route applies zero points in each partial pass and only sums in the combine."
445
+ },
446
+ "inputs": {
447
+ "aT": {
448
+ "dtype": "float32",
449
+ "shape": [128, 1024],
450
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 0.35 }
451
+ },
452
+ "bT": {
453
+ "dtype": "uint8",
454
+ "shape": [1024, 32, 32],
455
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/mixed_weight_cycle_b_t" } }
456
+ },
457
+ "scalesT": {
458
+ "dtype": "float32",
459
+ "shape": [1024, 32],
460
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
461
+ },
462
+ "zeroPointsT": {
463
+ "dtype": "float32",
464
+ "shape": [1024, 32],
465
+ "data": { "kind": "cycle", "values": [120.0, 132.0, 128.0, 140.0, 135.0, 118.0, 144.0, 126.0] }
466
+ }
467
+ },
468
+ "outputs": { "yT": { "dtype": "float32", "shape": [128, 1024], "tolerance": 0.005 } },
469
+ "attrs": { "K": 1024, "N": 1024, "bits": 8, "block_size": 32 }
470
+ },
471
+ {
472
+ "name": "q8_bias_only_prefill_reg_vec4_splitk_m128_k1024_n1024",
473
+ "provenance": {
474
+ "notes": "With bias and the implicit zero point, the split-K four-wide route adds bias once after combining the partial sums."
475
+ },
476
+ "inputs": {
477
+ "aT": {
478
+ "dtype": "float32",
479
+ "shape": [128, 1024],
480
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 0.35 }
481
+ },
482
+ "bT": {
483
+ "dtype": "uint8",
484
+ "shape": [1024, 32, 32],
485
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/mixed_weight_cycle_b_t" } }
486
+ },
487
+ "scalesT": {
488
+ "dtype": "float32",
489
+ "shape": [1024, 32],
490
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
491
+ },
492
+ "biasT": {
493
+ "dtype": "float32",
494
+ "shape": [1024],
495
+ "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41 }
496
+ }
497
+ },
498
+ "outputs": { "yT": { "dtype": "float32", "shape": [128, 1024], "tolerance": 0.005 } },
499
+ "attrs": { "K": 1024, "N": 1024, "bits": 8, "block_size": 32 }
500
+ },
501
  {
502
  "name": "q8_zero_bias_prefill_sgmat_m32_storage_floor",
503
+ "provenance": {
504
+ "notes": "Q8 weights with explicit zero points and bias exercise the 32-row subgroup-matrix tile at the 16 KiB storage floor."
505
+ },
506
  "inputs": {
507
  "aT": {
508
  "dtype": "float32",
 
815
  }
816
  },
817
  "outputs": { "yT": { "dtype": "float16", "shape": [512, 1024], "tolerance": 0.0001, "relTolerance": 0.001 } },
818
+ "provenance": { "notes": "Float16 activations exercise float16 A/B staging on the register-tiled prefill path." },
 
 
819
  "attrs": { "K": 128, "N": 1024, "bits": 4, "block_size": 32 }
820
  },
821
  {
 
893
  },
894
  "outputs": { "yT": { "dtype": "float16", "shape": [512, 1024], "tolerance": 0.0001, "relTolerance": 0.001 } },
895
  "provenance": {
896
+ "notes": "Eight-bit weights give the widest dequantized range, exercising float16 staging and store rounding on the register-tiled prefill path."
897
  },
898
  "attrs": { "K": 128, "N": 1024, "bits": 8, "block_size": 32 }
899
  },
 
957
  {
958
  "name": "q4_gemv_default_zero_m1_tail_n7",
959
  "provenance": {
960
+ "notes": "M=1 and N=7 leave three live columns in the final four-column output group. The guarded stores must write those columns without accessing the eighth column; zero points are omitted."
961
  },
962
  "inputs": {
963
  "aT": {
 
982
  {
983
  "name": "q4_gemv_default_zero_m1_odd_k33_n5",
984
  "provenance": {
985
+ "notes": "With K=33, the last stored byte of the second q4 block contains one live nibble and one beyond K. The GEMV must guard the out-of-range activation."
986
  },
987
  "inputs": {
988
  "aT": {
 
1007
  {
1008
  "name": "q2_gemv_default_zero_m1_k18_n5",
1009
  "provenance": {
1010
+ "notes": "Four q2 codes share each byte, and K=18 leaves two codes in the final byte beyond K. The GEMV must unpack the live codes and guard both out-of-range activations."
1011
  },
1012
  "inputs": {
1013
  "aT": {
 
1059
  "name": "q4_gemv_default_zero_m1_tail_n7_ncols8",
1060
  "tunables": { "GEMV_N_COLS": 8 },
1061
  "provenance": {
1062
+ "notes": "With `GEMV_N_COLS = 8` and N=7, one workgroup has active columns 4 through 6 and a fully guarded column 7 in its second group."
1063
  },
1064
  "inputs": {
1065
  "aT": {
 
1084
  {
1085
  "name": "q8_zero_bias_gemv_m1_tail_n5",
1086
  "provenance": {
1087
+ "notes": "M=1 and N=5 leave one live column in the final four-column output group. The case combines guarded tail stores with q8 unpacking, explicit zero points, and the bias epilogue."
1088
  },
1089
  "inputs": {
1090
  "aT": {
 
1121
  "provenance": {
1122
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1123
  "test": "independent has_zero_points/has_bias epilogues",
1124
+ "notes": "An M=1 q8 GEMV with explicit zero points and no bias isolates the zero-point binding layout and epilogue."
1125
  },
1126
  "inputs": {
1127
  "aT": {
 
1153
  "provenance": {
1154
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1155
  "test": "independent has_zero_points/has_bias epilogues",
1156
+ "notes": "An M=1 q8 GEMV with bias and the schema-default zero point isolates the bias binding layout and epilogue."
1157
  },
1158
  "inputs": {
1159
  "aT": {
 
1184
  "name": "q8_zero_bias_gemv_m1_tail_n5_ncols8",
1185
  "tunables": { "GEMV_N_COLS": 8 },
1186
  "provenance": {
1187
+ "notes": "With `GEMV_N_COLS = 8` and N=5, q8 unpacking, explicit zero points, and bias run with one live column in the second group."
1188
  },
1189
  "inputs": {
1190
  "aT": {
 
1219
  {
1220
  "name": "q8_zero_bias_naive_fallback_tailK_m3_n6",
1221
  "provenance": {
1222
+ "notes": "K=17 with block size 16 leaves a partial block; M=3 bypasses GEMV and N=6 remains below the tiled floors. The scalar fallback handles q8 unpacking, per-block zero points, bias, and the K tail."
1223
  },
1224
  "inputs": {
1225
  "aT": {
 
1282
  {
1283
  "name": "q4_prefill_tiled_reg_aligned_m32_k32_n4096_at_workgroup_floor",
1284
  "provenance": {
1285
+ "source": "synthetic",
1286
+ "notes": "Aligned K=32, M=32, and N=4096 produce exactly 64 workgroups, exercising the register-tiled prefill route at its workgroup floor."
1287
  },
1288
  "inputs": {
1289
  "aT": {
 
1311
  {
1312
  "name": "q4_prefill_tiled_reg_tailk_m32_k33_n512_below_wide_n_floor",
1313
  "provenance": {
1314
+ "notes": "K=33 exercises a padded final block while N=512 places the register-tiled prefill route in its relaxed narrow-output band."
1315
  },
1316
  "inputs": {
1317
  "aT": {
 
1394
  "provenance": {
1395
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1396
  "test": "independent has_zero_points/has_bias epilogues",
1397
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1398
  },
1399
  "inputs": {
1400
  "aT": {
 
1426
  "provenance": {
1427
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1428
  "test": "independent has_zero_points/has_bias epilogues",
1429
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1430
  },
1431
  "inputs": {
1432
  "aT": {
 
1481
  "provenance": {
1482
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1483
  "test": "independent has_zero_points/has_bias epilogues",
1484
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1485
  },
1486
  "attrs": { "K": 128, "N": 64, "bits": 8, "block_size": 32 }
1487
  },
 
1513
  "provenance": {
1514
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1515
  "test": "independent has_zero_points/has_bias epilogues",
1516
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1517
  },
1518
  "attrs": { "K": 128, "N": 64, "bits": 8, "block_size": 32 }
1519
  },
 
1545
  "provenance": {
1546
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1547
  "test": "independent has_zero_points/has_bias epilogues",
1548
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1549
  },
1550
  "attrs": { "K": 128, "N": 1024, "bits": 4, "block_size": 32 }
1551
  },
 
1577
  "provenance": {
1578
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1579
  "test": "independent has_zero_points/has_bias epilogues",
1580
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1581
  },
1582
  "attrs": { "K": 128, "N": 1024, "bits": 4, "block_size": 32 }
1583
  },
 
1609
  "provenance": {
1610
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1611
  "test": "independent has_zero_points/has_bias epilogues",
1612
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1613
  },
1614
  "attrs": { "K": 128, "N": 1024, "bits": 8, "block_size": 32 }
1615
  },
 
1641
  "provenance": {
1642
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1643
  "test": "independent has_zero_points/has_bias epilogues",
1644
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1645
  },
1646
  "attrs": { "K": 128, "N": 1024, "bits": 8, "block_size": 32 }
1647
  },
 
1650
  "provenance": {
1651
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1652
  "test": "independent has_zero_points/has_bias epilogues",
1653
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1654
  },
1655
  "inputs": {
1656
  "aT": {
 
1682
  "provenance": {
1683
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1684
  "test": "independent has_zero_points/has_bias epilogues",
1685
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1686
  },
1687
  "inputs": {
1688
  "aT": {
 
1714
  "provenance": {
1715
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1716
  "test": "independent has_zero_points/has_bias epilogues",
1717
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1718
  },
1719
  "inputs": {
1720
  "aT": {
 
1746
  "provenance": {
1747
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1748
  "test": "independent has_zero_points/has_bias epilogues",
1749
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1750
  },
1751
  "inputs": {
1752
  "aT": {
 
1801
  "provenance": {
1802
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1803
  "test": "independent has_zero_points/has_bias epilogues",
1804
+ "notes": "The only optional input is zeroPointsT; the expected output isolates explicit zero-point decoding from bias handling."
1805
  },
1806
  "attrs": { "K": 128, "N": 64, "bits": 4, "block_size": 32 }
1807
  },
 
1833
  "provenance": {
1834
  "source": "onnxruntime/contrib_ops/webgpu/quantization/matmul_nbits.cc",
1835
  "test": "independent has_zero_points/has_bias epilogues",
1836
+ "notes": "The only optional input is biasT; zero-point decoding uses the schema default, isolating the bias epilogue."
1837
  },
1838
  "attrs": { "K": 128, "N": 64, "bits": 4, "block_size": 32 }
1839
  },
 
1859
  "provenance": {
1860
  "source": "onnxruntime/contrib_ops/webgpu/quantization/dp4a_matmul_nbits.cc",
1861
  "test": "accuracy_level=4 int8 activation quantization (DP4AMatMulQuantize + DP4AMatMulNBits)",
1862
+ "notes": "Accuracy level 4 quantizes A per 128-element block using `max_abs / 127` and signed-normalized rounding, then multiplies q4 weights with packed integer dots. Tolerances account for the permitted activation-quantization error."
1863
  },
1864
  "attrs": { "K": 256, "N": 64, "bits": 4, "block_size": 32, "accuracy_level": 4 }
1865
  },
 
1874
  "bT": {
1875
  "dtype": "uint8",
1876
  "shape": [16, 2, 32],
1877
+ "data": { "kind": "cycle", "values": [171, 205, 46, 251, 218, 201, 31, 186, 237, 220, 154] }
1878
  },
1879
  "scalesT": { "dtype": "float32", "shape": [16, 2], "data": { "kind": "linspace", "start": 0.02, "end": 0.06 } }
1880
  },
 
1882
  "provenance": {
1883
  "source": "onnxruntime/contrib_ops/webgpu/quantization/dp4a_matmul_nbits.cc",
1884
  "test": "accuracy_level=4 int8 activation quantization (DP4AMatMulQuantize + DP4AMatMulNBits)",
1885
+ "notes": "Diverges from the upstream test's inputs (inputs.bT cycle [37, 201, 116, 88, 250, 13, 164, 95, 142, 7, 233] -> cycle [171, 205, 46, 251, 218, 201, 31, 186, 237, 220, 154]); the expected output is recomputed by the CPU reference for the new inputs. Accuracy level 4 quantizes A per 128-element block using `max_abs / 127` and signed-normalized rounding, then multiplies q4 weights with packed integer dots. Tolerances account for the permitted activation-quantization error, so the weight codes sit mostly above the default zero point: a near-zero-mean code cycle cancels the product down to the magnitude of that error, leaving no tolerance that can see a scale mistake."
1886
  },
1887
  "attrs": { "K": 128, "N": 16, "bits": 4, "block_size": 64, "accuracy_level": 4 }
1888
  },
 
2133
  },
2134
  "outputs": { "yT": { "dtype": "float32", "shape": [3, 6], "tolerance": 0.001 } },
2135
  "attrs": { "K": 17, "N": 6, "bits": 8, "block_size": 16 }
2136
+ },
2137
+ {
2138
+ "name": "q4_prefill_tile4x4_m4_k128_n4100_column_groups_partial_group",
2139
+ "provenance": {
2140
+ "notes": "K=128 at block 32 is sixteen packed words per column, so the small-M kernel runs 16 K-lanes and four column groups (16 columns) per workgroup. N=4100 is 256 full workgroups plus one whose second group starts at column 4096 and whose third and fourth groups lie past N, so the group-valid guard and the partial last group are both exercised at scale; M=4 is one row tile."
2141
+ },
2142
+ "inputs": {
2143
+ "aT": {
2144
+ "dtype": "float32",
2145
+ "shape": [4, 128],
2146
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.23, "scale": 0.35 }
2147
+ },
2148
+ "bT": {
2149
+ "dtype": "uint8",
2150
+ "shape": [4100, 4, 16],
2151
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/q4_weight_cycle_b_t" } }
2152
+ },
2153
+ "scalesT": {
2154
+ "dtype": "float32",
2155
+ "shape": [4100, 4],
2156
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
2157
+ }
2158
+ },
2159
+ "outputs": { "yT": { "dtype": "float32", "shape": [4, 4100], "tolerance": 0.0001 } },
2160
+ "attrs": { "K": 128, "N": 4100, "bits": 4, "block_size": 32 }
2161
+ },
2162
+ {
2163
+ "name": "q8_zero_bias_prefill_tile4x4_m5_k64_n300_column_groups",
2164
+ "provenance": {
2165
+ "notes": "q8 at K=64, block 32: sixteen packed words per column, four 8-bit codes per word, so a word carries four K-adjacent codes of one block. N=300 with sixteen columns per workgroup leaves a final workgroup whose last group holds columns 296..299 and M=5 leaves a second row tile with one live row, so the per-row and per-column guards run together with zero points and bias."
2166
+ },
2167
+ "inputs": {
2168
+ "aT": {
2169
+ "dtype": "float32",
2170
+ "shape": [5, 64],
2171
+ "data": { "kind": "fillFloat32", "sinStep": 0.09, "cosStep": 0.27, "scale": 0.4 }
2172
+ },
2173
+ "bT": {
2174
+ "dtype": "uint8",
2175
+ "shape": [300, 2, 32],
2176
+ "data": { "kind": "cycle", "values": [17, 200, 91, 45, 233, 128, 7, 176, 250, 33, 142, 99, 210, 64, 188] }
2177
+ },
2178
+ "scalesT": {
2179
+ "dtype": "float32",
2180
+ "shape": [300, 2],
2181
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
2182
+ },
2183
+ "zeroPointsT": {
2184
+ "dtype": "float32",
2185
+ "shape": [300, 2],
2186
+ "data": { "kind": "cycle", "values": [120.0, 132.0, 128.0, 140.0, 135.0, 118.0, 144.0] }
2187
+ },
2188
+ "biasT": {
2189
+ "dtype": "float32",
2190
+ "shape": [300],
2191
+ "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41, "scale": 0.1 }
2192
+ }
2193
+ },
2194
+ "outputs": { "yT": { "dtype": "float32", "shape": [5, 300], "tolerance": 0.001 } },
2195
+ "attrs": { "K": 64, "N": 300, "bits": 8, "block_size": 32 }
2196
+ },
2197
+ {
2198
+ "name": "q4_gemv_default_zero_m1_k2080_n6_two_vector_trips",
2199
+ "provenance": {
2200
+ "notes": "K=2080 at block size 32 yields 65 sixteen-byte vectors per column, so lane 0 makes a second vector-loop trip and applies the 65th block's scale. N=6 leaves the second column group half empty."
2201
+ },
2202
+ "inputs": {
2203
+ "aT": {
2204
+ "dtype": "float32",
2205
+ "shape": [1, 2080],
2206
+ "data": { "kind": "fillFloat32", "sinStep": 0.017, "cosStep": 0.031 }
2207
+ },
2208
+ "bT": {
2209
+ "dtype": "uint8",
2210
+ "shape": [6, 65, 16],
2211
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/q4_weight_cycle_b_t" } }
2212
+ },
2213
+ "scalesT": {
2214
+ "dtype": "float32",
2215
+ "shape": [6, 65],
2216
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
2217
+ }
2218
+ },
2219
+ "outputs": { "yT": { "dtype": "float32", "shape": [1, 6], "tolerance": 0.0001 } },
2220
+ "attrs": { "K": 2080, "N": 6, "bits": 4, "block_size": 32 }
2221
+ },
2222
+ {
2223
+ "name": "q8_gemv_zero_bias_m1_k48_block16_word_vectors",
2224
+ "provenance": {
2225
+ "notes": "q8 at block 16 is a sixteen-byte blob, so the GEMV still loads whole sixteen-byte vectors (one per block) while q4 at block 16 would fall to single words; K=48 is three vectors and N=9 leaves a partial column group, with zero points and bias."
2226
+ },
2227
+ "inputs": {
2228
+ "aT": {
2229
+ "dtype": "float32",
2230
+ "shape": [1, 48],
2231
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29 }
2232
+ },
2233
+ "bT": {
2234
+ "dtype": "uint8",
2235
+ "shape": [9, 3, 16],
2236
+ "data": { "kind": "cycle", "values": [17, 200, 91, 45, 233, 128, 7, 176, 250, 33, 142, 99, 210, 64, 188] }
2237
+ },
2238
+ "scalesT": {
2239
+ "dtype": "float32",
2240
+ "shape": [9, 3],
2241
+ "data": { "kind": "cycle", "values": [0.04, 0.055, 0.05, 0.065, 0.06, 0.075, 0.07] }
2242
+ },
2243
+ "zeroPointsT": {
2244
+ "dtype": "float32",
2245
+ "shape": [9, 3],
2246
+ "data": { "kind": "cycle", "values": [120.0, 132.0, 128.0, 140.0, 135.0, 118.0, 144.0] }
2247
+ },
2248
+ "biasT": {
2249
+ "dtype": "float32",
2250
+ "shape": [9],
2251
+ "data": { "kind": "fillFloat32", "sinStep": 0.07, "cosStep": 0.41, "scale": 0.1 }
2252
+ }
2253
+ },
2254
+ "outputs": { "yT": { "dtype": "float32", "shape": [1, 9], "tolerance": 0.0001 } },
2255
+ "attrs": { "K": 48, "N": 9, "bits": 8, "block_size": 16 }
2256
+ },
2257
+ {
2258
+ "name": "q4_gemv_default_zero_m1_block16_single_word_vectors_k40",
2259
+ "provenance": {
2260
+ "notes": "q4 at block 16 is an eight-byte blob, below the sixteen-byte vector, so the GEMV falls to one-word vectors (eight codes each) and K=40 ends half way through the third block: the last vector's trailing codes lie past K and must contribute zero."
2261
+ },
2262
+ "inputs": {
2263
+ "aT": {
2264
+ "dtype": "float32",
2265
+ "shape": [1, 40],
2266
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.37 }
2267
+ },
2268
+ "bT": {
2269
+ "dtype": "uint8",
2270
+ "shape": [5, 3, 8],
2271
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/q4_weight_cycle_b_t" } }
2272
+ },
2273
+ "scalesT": {
2274
+ "dtype": "float32",
2275
+ "shape": [5, 3],
2276
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/quant_scale_cycle_t" } }
2277
+ }
2278
+ },
2279
+ "outputs": { "yT": { "dtype": "float32", "shape": [1, 5], "tolerance": 0.0001 } },
2280
+ "attrs": { "K": 40, "N": 5, "bits": 4, "block_size": 16 }
2281
  }
2282
  ]
2283
  }