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

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,94 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # com.microsoft.MatMulBnb4
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Computes `A @ dequant(B)^T` where `B` uses bitsandbytes 4-bit quantization: `quant_type = 0` selects FP4 and `quant_type = 1` selects NF4. Supports rank-2 float16/float32 `A`, `transB = 1`, and `training_mode = 0`; rank-1 and rank-3-or-higher `A`, bfloat16, `transB = 0`, and training are not implemented. `B` is the flattened `[N, K]` weight, two codes per byte with the even flat index in the high nibble. Each code indexes a fixed 16-entry codebook, and the value is `codebook[code] * absmax[flat_index / block_size]`.
16
+
17
+ See the [ONNX Runtime `MatMulBnb4` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.MatMulBnb4) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `A` | `aT` | `T1` | `2` | — | Float input matrix of shape `(M, K)`, not quantized. | required |
24
+ | `B` | `bT` | `T2` | `1` | — | The `[N, K]` weight, flattened and quantized to 4 bits, stored as `(N * K + 1) / 2` bytes; the ONNX type is uint8 (this WebGPU implementation reads one widened u32 per stored byte). | required |
25
+ | `absmax` | `absmaxT` | `T1` | `1` | — | Per-block absolute-maximum dequantization scales of shape `((N * K + block_size - 1) / block_size)`, same dtype as A. | required |
26
+
27
+ ## Outputs
28
+
29
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
30
+ | --- | --- | --- | --- | --- | --- | --- |
31
+ | `Y` | `yT` | `T1` | `2` | `[A[0], N]` | Result of `A` multiplied by the dequantized, transposed weight matrix, with shape `(M, N)` and the same dtype as `A`. | required |
32
+
33
+ ## Attributes
34
+
35
+ Attributes and default values (overridable per request):
36
+
37
+ | Attribute | Default | Description |
38
+ | --- | --- | --- |
39
+ | `training_mode` | `0` | Whether training outputs are requested. This inference-only implementation supports the standard default value 0. |
40
+ | `transB` | `1` | Whether the quantized weight is stored transposed. This implementation supports the standard default value 1. |
41
+ | `K` | — | Input feature count (the shared dimension). |
42
+ | `N` | — | Output feature count. |
43
+ | `block_size` | — | Number of weights sharing one absmax scale; a power of two, at least 16. |
44
+ | `quant_type` | — | Codebook selector: 0 = FP4, 1 = NF4. |
45
+
46
+ ## Type constraints
47
+
48
+ | Variable | Allowed dtypes |
49
+ | --- | --- |
50
+ | `T1` | `float32`, `float16` |
51
+ | `T2` | `uint8` |
52
+
53
+ ## Device requirements
54
+
55
+ 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.
56
+
57
+ ## Files
58
+
59
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
60
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
61
+ - [`test.json`](build/webgpu/test.json) — correctness cases
62
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
63
+ - [`cast-scalar-x4.wgsl.jinja`](build/webgpu/cast-scalar-x4.wgsl.jinja)
64
+ - [`matmul-bnb4-gemv.wgsl.jinja`](build/webgpu/matmul-bnb4-gemv.wgsl.jinja)
65
+ - [`matmul-bnb4-sgmat.wgsl.jinja`](build/webgpu/matmul-bnb4-sgmat.wgsl.jinja)
66
+ - [`matmul-bnb4-tiled.wgsl.jinja`](build/webgpu/matmul-bnb4-tiled.wgsl.jinja)
67
+ - [`matmul-bnb4.wgsl.jinja`](build/webgpu/matmul-bnb4.wgsl.jinja)
68
+
69
+ ## Use with `@huggingface/kernels`
70
+
71
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
72
+ It then allocates the result tensors automatically.
73
+
74
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
75
+
76
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
77
+
78
+ ```js
79
+ import { getKernel } from "@huggingface/kernels";
80
+
81
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.MatMulBnb4", { version: 1 });
82
+ const { yT } = await kernel({
83
+ aT: { data: aTData, shape: [2, 24] },
84
+ bT: { data: bTData, shape: [36] },
85
+ absmaxT: { data: absmaxTData, shape: [5] },
86
+ }, {
87
+ attrs: {
88
+ K: 24,
89
+ N: 3,
90
+ block_size: 16,
91
+ quant_type: 1,
92
+ },
93
+ });
94
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.MatMulBnb4",
3
+ "cases": [
4
+ {
5
+ "name": "bnb4-nf4-decode-m1-k2048-n2048-b64",
6
+ "preset": "smoke",
7
+ "vars": { "M": 1, "K": 2048, "N": 2048, "blockSize": 64 },
8
+ "attrs": { "K": 2048, "N": 2048, "block_size": 64, "quant_type": 1 },
9
+ "inputs": {
10
+ "aT": { "shape": [1, 2048], "dtype": "float32", "dist": "normal", "seed": 410, "scale": 0.2 },
11
+ "bT": { "shape": [2097152], "dtype": "uint8", "dist": "linearMod", "mod": 256, "step": 7 },
12
+ "absmaxT": {
13
+ "shape": [65536],
14
+ "dtype": "float32",
15
+ "dist": "uniform",
16
+ "seed": 411,
17
+ "offset": 0.04,
18
+ "scale": 0.01,
19
+ "signed": false
20
+ }
21
+ },
22
+ "outputs": { "yT": { "shape": [1, 2048], "dtype": "float32" } },
23
+ "bench": {
24
+ "metrics": [
25
+ {
26
+ "type": "bandwidth",
27
+ "value": "numel(shapes.bT) * 4 + (numel(shapes.aT) + numel(shapes.absmaxT) + numel(shapes.yT)) * 4"
28
+ }
29
+ ]
30
+ }
31
+ },
32
+ {
33
+ "name": "bnb4-nf4-prefill-m128-k2048-n2048-b64",
34
+ "preset": "smoke",
35
+ "vars": { "M": 128, "K": 2048, "N": 2048, "blockSize": 64 },
36
+ "attrs": { "K": 2048, "N": 2048, "block_size": 64, "quant_type": 1 },
37
+ "tunableSpace": { "PORTABLE_TILE_K": [16, 32, 64] },
38
+ "inputs": {
39
+ "aT": { "shape": [128, 2048], "dtype": "float32", "dist": "normal", "seed": 412, "scale": 0.2 },
40
+ "bT": { "shape": [2097152], "dtype": "uint8", "dist": "linearMod", "mod": 256, "step": 7 },
41
+ "absmaxT": {
42
+ "shape": [65536],
43
+ "dtype": "float32",
44
+ "dist": "uniform",
45
+ "seed": 413,
46
+ "offset": 0.04,
47
+ "scale": 0.01,
48
+ "signed": false
49
+ }
50
+ },
51
+ "outputs": { "yT": { "shape": [128, 2048], "dtype": "float32" } },
52
+ "bench": {
53
+ "primary": true,
54
+ "metrics": [{ "type": "gflops", "value": "2 * dim(shapes.aT, 0) * args.K * args.N" }]
55
+ }
56
+ },
57
+ {
58
+ "name": "bnb4-fp4-prefill-m128-k2048-n2048-b64",
59
+ "preset": "smoke",
60
+ "vars": { "M": 128, "K": 2048, "N": 2048, "blockSize": 64 },
61
+ "attrs": { "K": 2048, "N": 2048, "block_size": 64, "quant_type": 0 },
62
+ "tunableSpace": { "PORTABLE_TILE_K": [16, 32, 64] },
63
+ "inputs": {
64
+ "aT": { "shape": [128, 2048], "dtype": "float32", "dist": "normal", "seed": 414, "scale": 0.2 },
65
+ "bT": { "shape": [2097152], "dtype": "uint8", "dist": "linearMod", "mod": 256, "step": 7 },
66
+ "absmaxT": {
67
+ "shape": [65536],
68
+ "dtype": "float32",
69
+ "dist": "uniform",
70
+ "seed": 415,
71
+ "offset": 0.04,
72
+ "scale": 0.01,
73
+ "signed": false
74
+ }
75
+ },
76
+ "outputs": { "yT": { "shape": [128, 2048], "dtype": "float32" } },
77
+ "bench": { "metrics": [{ "type": "gflops", "value": "2 * dim(shapes.aT, 0) * args.K * args.N" }] }
78
+ },
79
+ {
80
+ "name": "bnb4-nf4-prefill-m512-k4096-n4096-b64",
81
+ "preset": "model",
82
+ "vars": { "M": 512, "K": 4096, "N": 4096, "blockSize": 64 },
83
+ "attrs": { "K": 4096, "N": 4096, "block_size": 64, "quant_type": 1 },
84
+ "inputs": {
85
+ "aT": { "shape": [512, 4096], "dtype": "float32", "dist": "normal", "seed": 416, "scale": 0.2 },
86
+ "bT": { "shape": [8388608], "dtype": "uint8", "dist": "linearMod", "mod": 256, "step": 7 },
87
+ "absmaxT": {
88
+ "shape": [262144],
89
+ "dtype": "float32",
90
+ "dist": "uniform",
91
+ "seed": 417,
92
+ "offset": 0.04,
93
+ "scale": 0.01,
94
+ "signed": false
95
+ }
96
+ },
97
+ "outputs": { "yT": { "shape": [512, 4096], "dtype": "float32" } },
98
+ "bench": { "metrics": [{ "type": "gflops", "value": "2 * dim(shapes.aT, 0) * args.K * args.N" }] }
99
+ },
100
+ {
101
+ "name": "bnb4-nf4-prefill-m512-k4096-n4096-b64-f16",
102
+ "preset": "model",
103
+ "vars": { "M": 512, "K": 4096, "N": 4096, "blockSize": 64 },
104
+ "attrs": { "K": 4096, "N": 4096, "block_size": 64, "quant_type": 1 },
105
+ "inputs": {
106
+ "aT": { "shape": [512, 4096], "dtype": "float16", "dist": "normal", "seed": 416, "scale": 0.2 },
107
+ "bT": { "shape": [8388608], "dtype": "uint8", "dist": "linearMod", "mod": 256, "step": 7 },
108
+ "absmaxT": {
109
+ "shape": [262144],
110
+ "dtype": "float16",
111
+ "dist": "uniform",
112
+ "seed": 417,
113
+ "offset": 0.04,
114
+ "scale": 0.01,
115
+ "signed": false
116
+ }
117
+ },
118
+ "outputs": { "yT": { "shape": [512, 4096], "dtype": "float16" } },
119
+ "bench": { "metrics": [{ "type": "gflops", "value": "2 * dim(shapes.aT, 0) * args.K * args.N" }] }
120
+ }
121
+ ]
122
+ }
build/webgpu/cast-scalar-x4.wgsl.jinja ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+ {% if wrapNarrowInt %}
6
+
7
+ // ONNX float-to-int8/uint8 narrowing wraps modulo 256 rather than saturating.
8
+ // Preserve the low byte after truncation; the signed path then sign-extends it.
9
+ fn cast_wrap(v: f32) -> {{ outScalar }} {
10
+ let low = i32(v) & 0xFF;
11
+ return {{ outScalar }}({% if wrapSigned %}select(low, low - 256, low > 127){% else %}low{% endif %});
12
+ }
13
+
14
+ {% endif %}
15
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
16
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
17
+ // Four scalar conversions per invocation retain vector-like dispatch density even when the
18
+ // logical element count is not vec4 aligned. For a vec4 bulk + scalar tail plan, only lane zero
19
+ // of the tail dispatch runs and starts at the first element not covered by the packed pass.
20
+ let invocation = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
21
+ let base = invocation * 4u;
22
+ {% for lane in range(4) %}
23
+ if (base + {{ lane }}u < params.count) {
24
+ y[base + {{ lane }}u] = {% if wrapNarrowInt %}cast_wrap(f32(x[base + {{ lane }}u])){% else %}{{ outScalar }}(x[base + {{ lane }}u]){% endif %};
25
+ }
26
+ {% endfor %}
27
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "MatMulBnb4",
4
+ "sinceVersion": 1,
5
+ "description": "Computes `A @ dequant(B)^T` where `B` uses bitsandbytes 4-bit quantization: `quant_type = 0` selects FP4 and `quant_type = 1` selects NF4. Supports rank-2 float16/float32 `A`, `transB = 1`, and `training_mode = 0`; rank-1 and rank-3-or-higher `A`, bfloat16, `transB = 0`, and training are not implemented. `B` is the flattened `[N, K]` weight, two codes per byte with the even flat index in the high nibble. Each code indexes a fixed 16-entry codebook, and the value is `codebook[code] * absmax[flat_index / block_size]`.",
6
+ "inputs": [
7
+ { "role": "A", "dtype": "T1", "rank": 2, "description": "Float input matrix of shape `(M, K)`, not quantized." },
8
+ {
9
+ "role": "B",
10
+ "dtype": "T2",
11
+ "rank": 1,
12
+ "description": "The `[N, K]` weight, flattened and quantized to 4 bits, stored as `(N * K + 1) / 2` bytes; the ONNX type is uint8 (this WebGPU implementation reads one widened u32 per stored byte)."
13
+ },
14
+ {
15
+ "role": "absmax",
16
+ "dtype": "T1",
17
+ "rank": 1,
18
+ "description": "Per-block absolute-maximum dequantization scales of shape `((N * K + block_size - 1) / block_size)`, same dtype as A."
19
+ }
20
+ ],
21
+ "outputs": [
22
+ {
23
+ "role": "Y",
24
+ "dtype": "T1",
25
+ "rank": 2,
26
+ "shape": "[dim(shapes.A, 0), attrs.N]",
27
+ "description": "Result of `A` multiplied by the dequantized, transposed weight matrix, with shape `(M, N)` and the same dtype as `A`."
28
+ }
29
+ ],
30
+ "attributes": { "training_mode": 0, "transB": 1 },
31
+ "attributeConstraints": {
32
+ "K": { "required": true },
33
+ "N": { "required": true },
34
+ "block_size": { "required": true },
35
+ "quant_type": { "required": true, "values": [0, 1] },
36
+ "training_mode": { "values": [0] },
37
+ "transB": { "values": [1] }
38
+ },
39
+ "attributeDescriptions": {
40
+ "K": "Input feature count (the shared dimension).",
41
+ "N": "Output feature count.",
42
+ "block_size": "Number of weights sharing one absmax scale; a power of two, at least 16.",
43
+ "quant_type": "Codebook selector: 0 = FP4, 1 = NF4.",
44
+ "training_mode": "Whether training outputs are requested. This inference-only implementation supports the standard default value 0.",
45
+ "transB": "Whether the quantized weight is stored transposed. This implementation supports the standard default value 1."
46
+ },
47
+ "typeConstraints": { "T1": ["float32", "float16"], "T2": ["uint8"] },
48
+ "args": {
49
+ "aT": { "kind": "tensor", "semantic": "A", "role": "input" },
50
+ "bT": { "kind": "tensor", "semantic": "B", "role": "input" },
51
+ "absmaxT": { "kind": "tensor", "semantic": "absmax", "role": "input" },
52
+ "yT": { "kind": "tensor", "semantic": "Y", "role": "output" }
53
+ },
54
+ "tunables": {
55
+ "WORKGROUP_SIZE": 64,
56
+ "TILE_MIN_M": 16,
57
+ "PORTABLE_TILE_K": 16,
58
+ "SGMAT_TILE_ROWS": 64,
59
+ "SGMAT_TALL_TILE_ROWS": 128,
60
+ "SGMAT_TALL_MIN_M": 128,
61
+ "SGMAT_TILE_COLS": 64,
62
+ "SGMAT_TILE_K": 32
63
+ },
64
+ "bindingSets": {
65
+ "main": [
66
+ {
67
+ "name": "a",
68
+ "arg": "aT",
69
+ "semantic": "A",
70
+ "buffer": { "type": "read-only-storage" },
71
+ "elementType": "$aScalar"
72
+ },
73
+ { "name": "b", "arg": "bT", "semantic": "B", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
74
+ {
75
+ "name": "absmax",
76
+ "arg": "absmaxT",
77
+ "semantic": "absmax",
78
+ "buffer": { "type": "read-only-storage" },
79
+ "elementType": "$absmaxScalar",
80
+ "length": "$ABSMAX_LEN"
81
+ },
82
+ { "name": "y", "arg": "yT", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$aScalar" },
83
+ {
84
+ "name": "params",
85
+ "semantic": "kernel.params",
86
+ "buffer": { "type": "uniform" },
87
+ "struct": {
88
+ "name": "Params",
89
+ "fields": [
90
+ { "name": "rows", "type": "u32", "value": "dim(shapes.A, 0)" },
91
+ { "name": "K", "type": "u32", "value": "attrs.K" },
92
+ { "name": "N", "type": "u32", "value": "attrs.N" },
93
+ { "name": "blockSize", "type": "u32", "value": "attrs.block_size" }
94
+ ]
95
+ }
96
+ }
97
+ ],
98
+ "gemv": [
99
+ {
100
+ "name": "a",
101
+ "arg": "aT",
102
+ "semantic": "A",
103
+ "buffer": { "type": "read-only-storage" },
104
+ "elementType": "$aScalar"
105
+ },
106
+ { "name": "b", "arg": "bT", "semantic": "B", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
107
+ {
108
+ "name": "absmax",
109
+ "arg": "absmaxT",
110
+ "semantic": "absmax",
111
+ "buffer": { "type": "read-only-storage" },
112
+ "elementType": "$absmaxScalar",
113
+ "length": "$ABSMAX_LEN"
114
+ },
115
+ { "name": "y", "arg": "yT", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$aScalar" },
116
+ {
117
+ "name": "params",
118
+ "semantic": "kernel.params",
119
+ "buffer": { "type": "uniform" },
120
+ "struct": {
121
+ "name": "Params",
122
+ "fields": [
123
+ { "name": "K", "type": "u32", "value": "attrs.K" },
124
+ { "name": "N", "type": "u32", "value": "attrs.N" },
125
+ { "name": "blockSize", "type": "u32", "value": "attrs.block_size" }
126
+ ]
127
+ }
128
+ }
129
+ ],
130
+ "sgmat": [
131
+ {
132
+ "name": "a",
133
+ "arg": "aT",
134
+ "semantic": "A",
135
+ "buffer": { "type": "read-only-storage" },
136
+ "elementType": "$aScalar"
137
+ },
138
+ { "name": "b", "arg": "bT", "semantic": "B", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
139
+ {
140
+ "name": "absmax",
141
+ "arg": "absmaxT",
142
+ "semantic": "absmax",
143
+ "buffer": { "type": "read-only-storage" },
144
+ "elementType": "$absmaxScalar",
145
+ "length": "$ABSMAX_LEN"
146
+ },
147
+ { "name": "y", "arg": "yT", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$aScalar" }
148
+ ],
149
+ "castA": [
150
+ {
151
+ "name": "x",
152
+ "arg": "aT",
153
+ "semantic": "A",
154
+ "buffer": { "type": "read-only-storage" },
155
+ "elementType": "$srcScalar"
156
+ },
157
+ { "name": "y", "semantic": "aF32", "buffer": { "type": "storage" }, "elementType": "f32" },
158
+ {
159
+ "name": "params",
160
+ "semantic": "kernel.params",
161
+ "buffer": { "type": "uniform" },
162
+ "struct": { "name": "Params", "fields": [{ "name": "count", "type": "u32", "value": "numel(shapes.A)" }] }
163
+ }
164
+ ],
165
+ "sgmatWiden": [
166
+ { "name": "a", "semantic": "aF32", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
167
+ { "name": "b", "arg": "bT", "semantic": "B", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
168
+ {
169
+ "name": "absmax",
170
+ "arg": "absmaxT",
171
+ "semantic": "absmax",
172
+ "buffer": { "type": "read-only-storage" },
173
+ "elementType": "$absmaxScalar",
174
+ "length": "$ABSMAX_LEN"
175
+ },
176
+ { "name": "y", "semantic": "yF32", "buffer": { "type": "storage" }, "elementType": "f32" }
177
+ ],
178
+ "castY": [
179
+ { "name": "x", "semantic": "yF32", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
180
+ { "name": "y", "arg": "yT", "semantic": "Y", "buffer": { "type": "storage" }, "elementType": "$srcScalar" },
181
+ {
182
+ "name": "params",
183
+ "semantic": "kernel.params",
184
+ "buffer": { "type": "uniform" },
185
+ "struct": {
186
+ "name": "Params",
187
+ "fields": [{ "name": "count", "type": "u32", "value": "dim(shapes.A, 0) * attrs.N" }]
188
+ }
189
+ }
190
+ ]
191
+ },
192
+ "derive": {
193
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
194
+ "packedBytesExpected": "ceilDiv(attrs.N * attrs.K, 2)",
195
+ "absmaxCountExpected": "ceilDiv(attrs.N * attrs.K, attrs.block_size)",
196
+ "aFloatOk": "(tensorDtypes.A == \"float32\" or tensorDtypes.A == \"float16\") and f16Ok(tensorDtypes.A)",
197
+ "portableWorkgroupSize": "min(tunables.WORKGROUP_SIZE, deviceWorkgroupCap)",
198
+ "commonShapeValid": "ranks.A == 2 and ranks.B == 1 and ranks.absmax == 1 and ranks.Y == 2 and aFloatOk and tensorDtypes.B == \"uint8\" and tensorDtypes.absmax == tensorDtypes.A and tensorDtypes.Y == tensorDtypes.A and attrs.K > 0 and attrs.N > 0 and attrs.block_size >= 16 and pow2ceil(attrs.block_size) == attrs.block_size and dim(shapes.A, 1) == attrs.K and dim(shapes.B, 0) == packedBytesExpected and dim(shapes.absmax, 0) == absmaxCountExpected and dim(shapes.Y, 0) == dim(shapes.A, 0) and dim(shapes.Y, 1) == attrs.N",
199
+ "gemvShapeValid": "commonShapeValid and dim(shapes.A, 0) == 1",
200
+ "portableWorkgroupFits": "portableWorkgroupSize > 0 and portableWorkgroupSize * 16 <= device.limits.maxComputeWorkgroupStorageSize",
201
+ "portableTileKValid": "tunables.PORTABLE_TILE_K >= 8 and tunables.PORTABLE_TILE_K % 8 == 0",
202
+ "tileEligible": "commonShapeValid and dim(shapes.A, 0) >= tunables.TILE_MIN_M and portableTileKValid",
203
+ "tileWorkgroupStorageBytes": "8 * 64 * tunables.PORTABLE_TILE_K",
204
+ "tileWorkgroupFits": "16 <= device.limits.maxComputeWorkgroupSizeX and 8 <= device.limits.maxComputeWorkgroupSizeY and 128 <= device.limits.maxComputeInvocationsPerWorkgroup and tileWorkgroupStorageBytes <= device.limits.maxComputeWorkgroupStorageSize and ceilDiv(attrs.N, 64) <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(dim(shapes.A, 0), 64) <= device.limits.maxComputeWorkgroupsPerDimension",
205
+ "sgmatMatrixSize": "8",
206
+ "sgmatTileRows": "tunables.SGMAT_TALL_TILE_ROWS if dim(shapes.A, 0) >= tunables.SGMAT_TALL_MIN_M else tunables.SGMAT_TILE_ROWS",
207
+ "sgmatRowSubtiles": "4",
208
+ "sgmatSubRows": "sgmatTileRows / sgmatRowSubtiles",
209
+ "sgmatSubCols": "4 * sgmatMatrixSize",
210
+ "sgmatLoadWidth": "sgmatMatrixSize",
211
+ "sgmatColSubtiles": "tunables.SGMAT_TILE_COLS / sgmatSubCols",
212
+ "sgmatNumSubgroups": "sgmatRowSubtiles * sgmatColSubtiles",
213
+ "sgmatSubgroupSize": "device.adapterInfo.subgroupMinSize if has(device.adapterInfo, \"subgroupMinSize\") else 1",
214
+ "sgmatWorkgroupSize": "sgmatNumSubgroups * sgmatSubgroupSize",
215
+ "sgmatBLoadsPerRow": "tunables.SGMAT_TILE_K / sgmatLoadWidth",
216
+ "sgmatWorkgroupStorageBytes": "4 * tunables.SGMAT_TILE_COLS * tunables.SGMAT_TILE_K",
217
+ "sgmatDispatchN": "ceilDiv(attrs.N, tunables.SGMAT_TILE_COLS)",
218
+ "sgmatDispatchM": "ceilDiv(dim(shapes.A, 0), sgmatTileRows)",
219
+ "sgmatWorkgroupFits": "sgmatWorkgroupSize <= deviceWorkgroupCap and sgmatWorkgroupStorageBytes <= device.limits.maxComputeWorkgroupStorageSize and sgmatDispatchN <= device.limits.maxComputeWorkgroupsPerDimension and sgmatDispatchM <= device.limits.maxComputeWorkgroupsPerDimension",
220
+ "sgmatWidenBytes": "numel(shapes.A) * 4",
221
+ "sgmatWidenOutBytes": "dim(shapes.A, 0) * attrs.N * 4",
222
+ "sgmatWidenFits": "sgmatWidenBytes <= device.limits.maxStorageBufferBindingSize and sgmatWidenBytes <= device.limits.maxBufferSize and sgmatWidenOutBytes <= device.limits.maxStorageBufferBindingSize and sgmatWidenOutBytes <= device.limits.maxBufferSize"
223
+ },
224
+ "constants": { "quantType": "attrs.quant_type", "ABSMAX_LEN": "ceilDiv(attrs.N * attrs.K, attrs.block_size)" },
225
+ "variants": [
226
+ {
227
+ "id": "gemv",
228
+ "priority": 20,
229
+ "when": ["gemvShapeValid", "portableWorkgroupFits"],
230
+ "constants": {
231
+ "workgroupSize": "portableWorkgroupSize",
232
+ "aScalar": "\"f16\" if tensorDtypes.A == \"float16\" else \"f32\"",
233
+ "usesF16": "tensorDtypes.A == \"float16\"",
234
+ "absmaxScalar": "\"f16\" if tensorDtypes.absmax == \"float16\" else \"f32\""
235
+ },
236
+ "passes": [
237
+ {
238
+ "id": "main",
239
+ "shader": "matmul-bnb4-gemv.wgsl.jinja",
240
+ "bindings": "gemv",
241
+ "dispatch": { "workgroups": "attrs.N" }
242
+ }
243
+ ]
244
+ },
245
+ {
246
+ "id": "sgmat",
247
+ "priority": 15,
248
+ "requires": {
249
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
250
+ "limits": { "maxComputeWorkgroupStorageSize": 8192 },
251
+ "subgroupMinSize": 32,
252
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
253
+ },
254
+ "when": ["commonShapeValid", "tensorDtypes.A == \"float32\"", "dim(shapes.A, 0) >= sgmatTileRows", "dim(shapes.A, 0) % sgmatTileRows == 0", "attrs.N % tunables.SGMAT_TILE_COLS == 0", "attrs.K % tunables.SGMAT_TILE_K == 0", "attrs.K % attrs.block_size == 0", "attrs.block_size % sgmatLoadWidth == 0", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize == 32", "device.adapterInfo.subgroupMaxSize == 32", "sgmatWorkgroupFits"],
255
+ "constants": {
256
+ "K": "attrs.K",
257
+ "N": "attrs.N",
258
+ "blockSize": "attrs.block_size",
259
+ "tileRows": "sgmatTileRows",
260
+ "tileCols": "tunables.SGMAT_TILE_COLS",
261
+ "tileK": "tunables.SGMAT_TILE_K",
262
+ "subRows": "sgmatSubRows",
263
+ "subCols": "sgmatSubCols",
264
+ "matrixSize": "sgmatMatrixSize",
265
+ "rowMatrices": "sgmatSubRows / sgmatMatrixSize",
266
+ "colMatrices": "sgmatSubCols / sgmatMatrixSize",
267
+ "loadWidth": "sgmatLoadWidth",
268
+ "rowSubtiles": "sgmatRowSubtiles",
269
+ "workgroupSize": "sgmatWorkgroupSize",
270
+ "bLoadsPerRow": "sgmatBLoadsPerRow",
271
+ "aScalar": "\"f32\"",
272
+ "usesF16": false,
273
+ "absmaxScalar": "\"f16\" if tensorDtypes.absmax == \"float16\" else \"f32\""
274
+ },
275
+ "passes": [
276
+ {
277
+ "id": "main",
278
+ "shader": "matmul-bnb4-sgmat.wgsl.jinja",
279
+ "bindings": "sgmat",
280
+ "dispatch": { "x": "sgmatDispatchN", "y": "sgmatDispatchM" }
281
+ }
282
+ ]
283
+ },
284
+ {
285
+ "id": "sgmat_widened",
286
+ "priority": 16,
287
+ "requires": {
288
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
289
+ "limits": { "maxComputeWorkgroupStorageSize": 8192 },
290
+ "subgroupMinSize": 32,
291
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
292
+ },
293
+ "when": ["commonShapeValid", "tensorDtypes.A == \"float16\"", "dim(shapes.A, 0) >= sgmatTileRows", "dim(shapes.A, 0) % sgmatTileRows == 0", "attrs.N % tunables.SGMAT_TILE_COLS == 0", "attrs.K % tunables.SGMAT_TILE_K == 0", "attrs.K % attrs.block_size == 0", "attrs.block_size % sgmatLoadWidth == 0", "has(device.adapterInfo, \"subgroupMinSize\")", "has(device.adapterInfo, \"subgroupMaxSize\")", "device.adapterInfo.subgroupMinSize == 32", "device.adapterInfo.subgroupMaxSize == 32", "sgmatWorkgroupFits", "sgmatWidenFits"],
294
+ "constants": {
295
+ "K": "attrs.K",
296
+ "N": "attrs.N",
297
+ "blockSize": "attrs.block_size",
298
+ "tileRows": "sgmatTileRows",
299
+ "tileCols": "tunables.SGMAT_TILE_COLS",
300
+ "tileK": "tunables.SGMAT_TILE_K",
301
+ "subRows": "sgmatSubRows",
302
+ "subCols": "sgmatSubCols",
303
+ "matrixSize": "sgmatMatrixSize",
304
+ "rowMatrices": "sgmatSubRows / sgmatMatrixSize",
305
+ "colMatrices": "sgmatSubCols / sgmatMatrixSize",
306
+ "loadWidth": "sgmatLoadWidth",
307
+ "rowSubtiles": "sgmatRowSubtiles",
308
+ "workgroupSize": "sgmatWorkgroupSize",
309
+ "bLoadsPerRow": "sgmatBLoadsPerRow",
310
+ "aScalar": "\"f32\"",
311
+ "usesF16": true,
312
+ "absmaxScalar": "\"f16\" if tensorDtypes.absmax == \"float16\" else \"f32\"",
313
+ "srcScalar": "\"f16\"",
314
+ "outScalar": "\"f32\"",
315
+ "wrapNarrowInt": false,
316
+ "wrapSigned": false
317
+ },
318
+ "passes": [
319
+ {
320
+ "id": "widen_a",
321
+ "name": "MatMulBnb4.WidenActivations",
322
+ "shader": "cast-scalar-x4.wgsl.jinja",
323
+ "bindings": "castA",
324
+ "dispatch": { "threads": "ceilDiv(numel(shapes.A), 4)", "workgroupSize": "tunables.WORKGROUP_SIZE" }
325
+ },
326
+ {
327
+ "id": "main",
328
+ "name": "MatMulBnb4.SubgroupMatrixWidened",
329
+ "shader": "matmul-bnb4-sgmat.wgsl.jinja",
330
+ "bindings": "sgmatWiden",
331
+ "dispatch": { "x": "sgmatDispatchN", "y": "sgmatDispatchM" }
332
+ },
333
+ {
334
+ "id": "narrow_y",
335
+ "name": "MatMulBnb4.NarrowOutput",
336
+ "shader": "cast-scalar-x4.wgsl.jinja",
337
+ "bindings": "castY",
338
+ "constants": { "outScalar": "\"f16\"" },
339
+ "dispatch": {
340
+ "threads": "ceilDiv(dim(shapes.A, 0) * attrs.N, 4)",
341
+ "workgroupSize": "tunables.WORKGROUP_SIZE"
342
+ }
343
+ }
344
+ ],
345
+ "description": "Runs the subgroup-matrix tier for a float16 request by widening the activations to float32 either side of the multiply. The matrix units this operator uses accumulate in float32 from float32 operands; the device's float16 configuration returns a float16 result, so feeding them float16 directly would drop the accumulator's precision.",
346
+ "intermediates": [
347
+ { "id": "aF32", "dtype": "float32", "shape": "[numel(shapes.A)]" },
348
+ { "id": "yF32", "dtype": "float32", "shape": "[dim(shapes.A, 0) * attrs.N]" }
349
+ ]
350
+ },
351
+ {
352
+ "id": "tiled",
353
+ "priority": 10,
354
+ "when": ["tileEligible", "tileWorkgroupFits"],
355
+ "constants": {
356
+ "tileK": "tunables.PORTABLE_TILE_K",
357
+ "aScalar": "\"f16\" if tensorDtypes.A == \"float16\" else \"f32\"",
358
+ "usesF16": "tensorDtypes.A == \"float16\"",
359
+ "absmaxScalar": "\"f16\" if tensorDtypes.absmax == \"float16\" else \"f32\""
360
+ },
361
+ "passes": [
362
+ {
363
+ "id": "main",
364
+ "shader": "matmul-bnb4-tiled.wgsl.jinja",
365
+ "bindings": "main",
366
+ "dispatch": { "x": "ceilDiv(attrs.N, 64)", "y": "ceilDiv(dim(shapes.A, 0), 64)" }
367
+ }
368
+ ]
369
+ },
370
+ {
371
+ "id": "scalar",
372
+ "priority": 0,
373
+ "when": ["commonShapeValid", "portableWorkgroupSize > 0"],
374
+ "constants": {
375
+ "workgroupSize": "portableWorkgroupSize",
376
+ "aScalar": "\"f16\" if tensorDtypes.A == \"float16\" else \"f32\"",
377
+ "usesF16": "tensorDtypes.A == \"float16\"",
378
+ "absmaxScalar": "\"f16\" if tensorDtypes.absmax == \"float16\" else \"f32\""
379
+ },
380
+ "passes": [
381
+ {
382
+ "id": "main",
383
+ "shader": "matmul-bnb4.wgsl.jinja",
384
+ "bindings": "main",
385
+ "dispatch": { "threads": "numel(shapes.Y)", "workgroupSize": "constants.workgroupSize" }
386
+ }
387
+ ]
388
+ }
389
+ ]
390
+ }
build/webgpu/matmul-bnb4-gemv.wgsl.jinja ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+
6
+ const WG: u32 = {{ workgroupSize }}u;
7
+
8
+ var<workgroup> partials: array<f32, {{ workgroupSize }}>;
9
+
10
+ /* bitsandbytes 4-bit weight decoding.
11
+
12
+ Only the selected FP4 or NF4 codebook is emitted. The codebook uses
13
+ var<private> rather than const because its runtime index requires a memory
14
+ location in WGSL.
15
+
16
+ FP4 is sign-magnitude (bit 3 is sign), so its table is non-monotonic. NF4 is
17
+ the ascending normal-float quantile codebook. Each stored byte is carried in
18
+ one widened u32. A flat weight index maps to byte flat >> 1, and an even
19
+ index reads the high nibble, matching (vi0 << 4) | vi1 packing. */
20
+ {% if quantType == 0 %}
21
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
22
+ 0.0, 5.208333333e-03, 0.66666667, 1.0,
23
+ 0.33333333, 0.5, 0.16666667, 0.25,
24
+ -0.0, -5.208333333e-03, -0.66666667, -1.0,
25
+ -0.33333333, -0.5, -0.16666667, -0.25
26
+ );
27
+ {%- else %}
28
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
29
+ -1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453,
30
+ -0.28444138169288635, -0.18477343022823334, -0.09105003625154495, 0.0,
31
+ 0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224,
32
+ 0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0
33
+ );
34
+ {%- endif %}
35
+
36
+ // One dequantized weight at flat index `flat` into the [N, K] weight.
37
+ fn dequant_weight(flat: u32) -> f32 {
38
+ let byte = b[flat >> 1u];
39
+ let code = select(byte & 0xFu, (byte >> 4u) & 0xFu, (flat & 1u) == 0u);
40
+ return quant_map[code] * f32(absmax[flat / params.blockSize]);
41
+ }
42
+
43
+
44
+
45
+ // Decode (M == 1): one workgroup owns each output column, with lanes striding K.
46
+ // Each lane keeps one accumulator and the workgroup closes with a tree reduction.
47
+ @compute @workgroup_size(WG, 1, 1)
48
+ fn main(
49
+ @builtin(workgroup_id) wid: vec3<u32>,
50
+ @builtin(num_workgroups) nwg: vec3<u32>,
51
+ @builtin(local_invocation_id) lid: vec3<u32>
52
+ ) {
53
+ let tid = lid.x;
54
+ // 2D-folded dispatch: wid.y carries the high bits past the per-dimension
55
+ // limit. The fold over-dispatches, so tail workgroups must leave before the
56
+ // first barrier — hence the early return here rather than a guarded write.
57
+ let col = wid.x + wid.y * nwg.x;
58
+ if (col >= params.N) {
59
+ return;
60
+ }
61
+
62
+ var acc = 0.0;
63
+ let b_row_base = col * params.K;
64
+ for (var k = tid; k < params.K; k = k + WG) {
65
+ acc = acc + f32(a[k]) * dequant_weight(b_row_base + k);
66
+ }
67
+ partials[tid] = acc;
68
+
69
+ // Ceil-halving tree reduce: WG is device-bounded and need not be
70
+ // a power of two, so the live count is carried explicitly. Plain `stride /= 2`
71
+ // would pair a live lane with an already-consumed slot at the first odd count.
72
+ var count = WG;
73
+ loop {
74
+ workgroupBarrier();
75
+ let half = (count + 1u) / 2u;
76
+ if (tid + half < count) {
77
+ partials[tid] = partials[tid] + partials[tid + half];
78
+ }
79
+ if (count == 1u) {
80
+ break;
81
+ }
82
+ count = half;
83
+ }
84
+
85
+ if (tid == 0u) {
86
+ y[col] = {{ aScalar }}(partials[0]);
87
+ }
88
+ }
build/webgpu/matmul-bnb4-sgmat.wgsl.jinja ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Large aligned prefill tier. The packed B tile is dequantized once per
2
+ // workgroup, then reused by subgroup-matrix f32 multiply-accumulate operations.
3
+ // This specialization operates on full A, B, and output tiles, so both
4
+ // subgroupMatrixLoad and the direct output stores are in bounds.
5
+ enable subgroups;
6
+ enable chromium_experimental_subgroup_matrix;
7
+ {% if usesF16 %}
8
+ // The widened path keeps the activations and the output in f32 for the matrix
9
+ // units, but the quantization scales stay in the request's own dtype.
10
+ enable f16;
11
+ {% endif %}
12
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
13
+
14
+ {{ env.wgsl.resourceDeclarations }}
15
+
16
+ const K: u32 = {{ K }}u;
17
+ const N: u32 = {{ N }}u;
18
+ const BLOCK_SIZE: u32 = {{ blockSize }}u;
19
+ const TILE_ROWS: u32 = {{ tileRows }}u;
20
+ const TILE_COLS: u32 = {{ tileCols }}u;
21
+ const TILE_K: u32 = {{ tileK }}u;
22
+ const SUB_ROWS: u32 = {{ subRows }}u;
23
+ const SUB_COLS: u32 = {{ subCols }}u;
24
+ const LOAD_WIDTH: u32 = {{ loadWidth }}u;
25
+
26
+ var<workgroup> tile_B: array<f32, {{ tileCols * tileK }}>;
27
+
28
+ /* bitsandbytes 4-bit weight decoding.
29
+
30
+ Only the selected FP4 or NF4 codebook is emitted. The codebook uses
31
+ var<private> rather than const because its runtime index requires a memory
32
+ location in WGSL.
33
+
34
+ FP4 is sign-magnitude (bit 3 is sign), so its table is non-monotonic. NF4 is
35
+ the ascending normal-float quantile codebook. Each stored byte is carried in
36
+ one widened u32. A flat weight index maps to byte flat >> 1, and an even
37
+ index reads the high nibble, matching (vi0 << 4) | vi1 packing. */
38
+ {% if quantType == 0 %}
39
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
40
+ 0.0, 5.208333333e-03, 0.66666667, 1.0,
41
+ 0.33333333, 0.5, 0.16666667, 0.25,
42
+ -0.0, -5.208333333e-03, -0.66666667, -1.0,
43
+ -0.33333333, -0.5, -0.16666667, -0.25
44
+ );
45
+ {%- else %}
46
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
47
+ -1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453,
48
+ -0.28444138169288635, -0.18477343022823334, -0.09105003625154495, 0.0,
49
+ 0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224,
50
+ 0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0
51
+ );
52
+ {%- endif %}
53
+
54
+
55
+
56
+
57
+ fn load_b(tile_base: u32, k_base: u32, row: u32, col_group: u32) {
58
+ let col = col_group * LOAD_WIDTH;
59
+ let flat = (tile_base + row) * K + k_base + col;
60
+ let scale = f32(absmax[flat / BLOCK_SIZE]);
61
+ let tile_offset = row * TILE_K + col;
62
+
63
+ // Eligibility guarantees that each aligned LOAD_WIDTH slice stays in one
64
+ // quantization block and begins on an even weight. The loader widens each
65
+ // packed byte once and reuses both nibbles.
66
+ for (var i = 0u; i < LOAD_WIDTH; i = i + 2u) {
67
+ let packed = b[(flat + i) >> 1u];
68
+ let high = (packed >> 4u) & 0xFu;
69
+ let low = packed & 0xFu;
70
+ tile_B[tile_offset + i] = quant_map[high] * scale;
71
+ tile_B[tile_offset + i + 1u] = quant_map[low] * scale;
72
+ }
73
+ }
74
+
75
+ @compute @workgroup_size({{ workgroupSize }}, 1, 1)
76
+ fn main(
77
+ @builtin(workgroup_id) workgroup_id: vec3<u32>,
78
+ @builtin(local_invocation_index) local_idx: u32,
79
+ @builtin(subgroup_size) subgroup_size: u32
80
+ ) {
81
+ let a_base = workgroup_id.y * TILE_ROWS;
82
+ let b_base = workgroup_id.x * TILE_COLS;
83
+
84
+ let subgroup_id = local_idx / subgroup_size;
85
+ let subtile_idx = subgroup_id / {{ rowSubtiles }}u;
86
+ let subtile_idy = subgroup_id % {{ rowSubtiles }}u;
87
+ let base_A = subtile_idy * SUB_ROWS;
88
+ let base_B = subtile_idx * SUB_COLS;
89
+
90
+ {% for row in range(rowMatrices) %}{% for col in range(colMatrices) %}
91
+ var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, {{ matrixSize }}, {{ matrixSize }}>;
92
+ {% endfor %}{% endfor %}
93
+
94
+ for (var k_base = 0u; k_base < K; k_base += TILE_K) {
95
+ load_b(b_base, k_base, local_idx / {{ bLoadsPerRow }}u, local_idx % {{ bLoadsPerRow }}u);
96
+ workgroupBarrier();
97
+
98
+ for (var step = 0u; step < TILE_K; step += {{ matrixSize }}u) {
99
+ let matrix_a_offset = (a_base + base_A) * K + k_base + step;
100
+ {% for row in range(rowMatrices) %}
101
+ let matA{{ row }} = subgroupMatrixLoad<subgroup_matrix_left<f32, {{ matrixSize }}, {{ matrixSize }}>>(
102
+ &a, matrix_a_offset + {{ row * matrixSize }}u * K, false, K
103
+ );
104
+ {% endfor %}
105
+ let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
106
+ {% for col in range(colMatrices) %}
107
+ let matB{{ col }} = subgroupMatrixLoad<subgroup_matrix_right<f32, {{ matrixSize }}, {{ matrixSize }}>>(
108
+ &tile_B, matrix_b_offset + {{ col * matrixSize }}u * TILE_K, true, TILE_K
109
+ );
110
+ {% endfor %}
111
+ {% for row in range(rowMatrices) %}{% for col in range(colMatrices) %}
112
+ matC{{ row }}{{ col }} = subgroupMatrixMultiplyAccumulate(matA{{ row }}, matB{{ col }}, matC{{ row }}{{ col }});
113
+ {% endfor %}{% endfor %}
114
+ }
115
+ workgroupBarrier();
116
+ }
117
+
118
+ let output_offset = (a_base + base_A) * N + b_base + base_B;
119
+ {% for row in range(rowMatrices) %}{% for col in range(colMatrices) %}
120
+ subgroupMatrixStore(
121
+ &y,
122
+ output_offset + {{ row * matrixSize }}u * N + {{ col * matrixSize }}u,
123
+ matC{{ row }}{{ col }},
124
+ false,
125
+ N
126
+ );
127
+ {% endfor %}{% endfor %}
128
+ }
build/webgpu/matmul-bnb4-tiled.wgsl.jinja ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+
6
+ // Prefill: register-blocked 64x64 shared-memory tiled GEMM (16x8 threads, each
7
+ // holding an 8x4 micro-tile, with K stepped in BK chunks).
8
+ //
9
+ // Each weight is dequantized once into the shared B tile and reused across the
10
+ // row block. Both operands are stored K-major — A is [M, K] and the packed
11
+ // weight is [N, K] — so both tiles group four K values per vector word, every
12
+ // staging read is contiguous, and one micro-tile step reads TM + TN vector
13
+ // words to issue TM * TN * 4 fused multiply-adds through dot().
14
+ const BK: u32 = {{ tileK }}u;
15
+ const BM: u32 = 64u;
16
+ const BN: u32 = 64u;
17
+ const TM: u32 = 8u;
18
+ const TN: u32 = 4u;
19
+ const WG_X: u32 = 16u;
20
+ const WG_SIZE: u32 = 128u;
21
+ const K_VECS: u32 = BK / 4u;
22
+
23
+ // Stage tileA at its input storage width; widening it on shared load is
24
+ // bit-identical. tileB remains f32 to avoid narrowing dequantized NF4/FP4
25
+ // values before accumulation.
26
+ {% set aTile = "f16" if usesF16 else "f32" %}
27
+ var<workgroup> tileA: array<array<vec4<{{ aTile }}>, K_VECS>, BM>;
28
+ var<workgroup> tileB: array<array<vec4<f32>, K_VECS>, BN>;
29
+
30
+ /* bitsandbytes 4-bit weight decoding.
31
+
32
+ Only the selected FP4 or NF4 codebook is emitted. The codebook uses
33
+ var<private> rather than const because its runtime index requires a memory
34
+ location in WGSL.
35
+
36
+ FP4 is sign-magnitude (bit 3 is sign), so its table is non-monotonic. NF4 is
37
+ the ascending normal-float quantile codebook. Each stored byte is carried in
38
+ one widened u32. A flat weight index maps to byte flat >> 1, and an even
39
+ index reads the high nibble, matching (vi0 << 4) | vi1 packing. */
40
+ {% if quantType == 0 %}
41
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
42
+ 0.0, 5.208333333e-03, 0.66666667, 1.0,
43
+ 0.33333333, 0.5, 0.16666667, 0.25,
44
+ -0.0, -5.208333333e-03, -0.66666667, -1.0,
45
+ -0.33333333, -0.5, -0.16666667, -0.25
46
+ );
47
+ {%- else %}
48
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
49
+ -1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453,
50
+ -0.28444138169288635, -0.18477343022823334, -0.09105003625154495, 0.0,
51
+ 0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224,
52
+ 0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0
53
+ );
54
+ {%- endif %}
55
+
56
+
57
+ // Four consecutive dequantized weights starting at flat index `flat` into the
58
+ // [N, K] weight. Consecutive indices share a stored byte, so the run costs two
59
+ // or three byte loads instead of four; the parity of `flat` decides which
60
+ // nibble opens the run.
61
+ fn dequant_weight4(flat: u32) -> vec4<f32> {
62
+ let base = flat >> 1u;
63
+ let byte0 = b[base];
64
+ let byte1 = b[base + 1u];
65
+ let byte2 = b[base + 2u];
66
+ var codes: vec4<u32>;
67
+ if ((flat & 1u) == 0u) {
68
+ codes = vec4<u32>((byte0 >> 4u) & 0xFu, byte0 & 0xFu, (byte1 >> 4u) & 0xFu, byte1 & 0xFu);
69
+ } else {
70
+ codes = vec4<u32>(byte0 & 0xFu, (byte1 >> 4u) & 0xFu, byte1 & 0xFu, (byte2 >> 4u) & 0xFu);
71
+ }
72
+ // block_size is a power of two of at least 16, so a four-weight run stays
73
+ // inside one block unless it starts within three of the block's end.
74
+ var scales: vec4<f32>;
75
+ if (flat % params.blockSize + 3u < params.blockSize) {
76
+ scales = vec4<f32>(f32(absmax[flat / params.blockSize]));
77
+ } else {
78
+ scales = vec4<f32>(
79
+ f32(absmax[flat / params.blockSize]),
80
+ f32(absmax[(flat + 1u) / params.blockSize]),
81
+ f32(absmax[(flat + 2u) / params.blockSize]),
82
+ f32(absmax[(flat + 3u) / params.blockSize])
83
+ );
84
+ }
85
+ return vec4<f32>(quant_map[codes.x], quant_map[codes.y], quant_map[codes.z], quant_map[codes.w]) * scales;
86
+ }
87
+
88
+
89
+ @compute @workgroup_size(16, 8, 1)
90
+ fn main(
91
+ @builtin(workgroup_id) wg: vec3<u32>,
92
+ @builtin(local_invocation_id) lid: vec3<u32>
93
+ ) {
94
+ let mBase = wg.y * BM;
95
+ let nBase = wg.x * BN;
96
+ let li = lid.y * WG_X + lid.x;
97
+ let aRow = lid.y * TM;
98
+ let bCol = lid.x * TN;
99
+
100
+ {% for row in range(8) %}
101
+ var acc{{ row }} = vec4<f32>(0.0);
102
+ {% endfor %}
103
+ let numTiles = (params.K + BK - 1u) / BK;
104
+ for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
105
+ let kBase = kt * BK;
106
+ // Each lane stages whole vector words: four consecutive K values of one A
107
+ // row and of one dequantized weight row.
108
+ for (var linear = li; linear < BM * K_VECS; linear += WG_SIZE) {
109
+ let ar = linear / K_VECS;
110
+ let ak4 = linear % K_VECS;
111
+ let am = mBase + ar;
112
+ let ak = kBase + ak4 * 4u;
113
+ var av = vec4<{{ aTile }}>({{ aTile }}(0.0));
114
+ if (am < params.rows) {
115
+ let aFlat = am * params.K + ak;
116
+ if (ak + 3u < params.K) {
117
+ av = vec4<{{ aTile }}>({{ aTile }}(a[aFlat]), {{ aTile }}(a[aFlat + 1u]), {{ aTile }}(a[aFlat + 2u]), {{ aTile }}(a[aFlat + 3u]));
118
+ } else {
119
+ for (var t = 0u; t < 4u; t = t + 1u) {
120
+ if (ak + t < params.K) {
121
+ av[t] = {{ aTile }}(a[aFlat + t]);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ tileA[ar][ak4] = av;
127
+ }
128
+ for (var linear = li; linear < BN * K_VECS; linear += WG_SIZE) {
129
+ let br = linear / K_VECS;
130
+ let bk4 = linear % K_VECS;
131
+ let bn = nBase + br;
132
+ let bk = kBase + bk4 * 4u;
133
+ var bv = vec4<f32>(0.0);
134
+ if (bn < params.N) {
135
+ bv = dequant_weight4(bn * params.K + bk);
136
+ if (bk + 3u >= params.K) {
137
+ for (var t = 0u; t < 4u; t = t + 1u) {
138
+ if (bk + t >= params.K) {
139
+ bv[t] = 0.0;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ tileB[br][bk4] = bv;
145
+ }
146
+ workgroupBarrier();
147
+ for (var kv: u32 = 0u; kv < K_VECS; kv = kv + 1u) {
148
+ {% for column in range(4) %}
149
+ let bv{{ column }} = tileB[bCol + {{ column }}u][kv];
150
+ {% endfor %}
151
+ {% for row in range(8) %}
152
+ let av{{ row }} = vec4<f32>(tileA[aRow + {{ row }}u][kv]);
153
+ acc{{ row }} += vec4<f32>({% for column in range(4) %}dot(av{{ row }}, bv{{ column }}){% if not loop.last %}, {% endif %}{% endfor %});
154
+ {% endfor %}
155
+ }
156
+ workgroupBarrier();
157
+ }
158
+
159
+ {% for row in range(8) %}
160
+ {% for column in range(4) %}
161
+ {
162
+ let m = mBase + aRow + {{ row }}u;
163
+ let n = nBase + bCol + {{ column }}u;
164
+ if (m < params.rows && n < params.N) { y[m * params.N + n] = {{ aScalar }}(acc{{ row }}.{{ ["x", "y", "z", "w"][column] }}); }
165
+ }
166
+ {% endfor %}
167
+ {% endfor %}
168
+ }
build/webgpu/matmul-bnb4.wgsl.jinja ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+
6
+ const WG: u32 = {{ workgroupSize }}u;
7
+
8
+ /* bitsandbytes 4-bit weight decoding.
9
+
10
+ Only the selected FP4 or NF4 codebook is emitted. The codebook uses
11
+ var<private> rather than const because its runtime index requires a memory
12
+ location in WGSL.
13
+
14
+ FP4 is sign-magnitude (bit 3 is sign), so its table is non-monotonic. NF4 is
15
+ the ascending normal-float quantile codebook. Each stored byte is carried in
16
+ one widened u32. A flat weight index maps to byte flat >> 1, and an even
17
+ index reads the high nibble, matching (vi0 << 4) | vi1 packing. */
18
+ {% if quantType == 0 %}
19
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
20
+ 0.0, 5.208333333e-03, 0.66666667, 1.0,
21
+ 0.33333333, 0.5, 0.16666667, 0.25,
22
+ -0.0, -5.208333333e-03, -0.66666667, -1.0,
23
+ -0.33333333, -0.5, -0.16666667, -0.25
24
+ );
25
+ {%- else %}
26
+ var<private> quant_map: array<f32, 16> = array<f32, 16>(
27
+ -1.0, -0.6961928009986877, -0.5250730514526367, -0.39491748809814453,
28
+ -0.28444138169288635, -0.18477343022823334, -0.09105003625154495, 0.0,
29
+ 0.07958029955625534, 0.16093020141124725, 0.24611230194568634, 0.33791524171829224,
30
+ 0.44070982933044434, 0.5626170039176941, 0.7229568362236023, 1.0
31
+ );
32
+ {%- endif %}
33
+
34
+ // One dequantized weight at flat index `flat` into the [N, K] weight.
35
+ fn dequant_weight(flat: u32) -> f32 {
36
+ let byte = b[flat >> 1u];
37
+ let code = select(byte & 0xFu, (byte >> 4u) & 0xFu, (flat & 1u) == 0u);
38
+ return quant_map[code] * f32(absmax[flat / params.blockSize]);
39
+ }
40
+
41
+
42
+
43
+ @compute @workgroup_size(WG, 1, 1)
44
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
45
+ // 2D-folded flat output-element index: gid.y carries the high bits past the
46
+ // maxComputeWorkgroupsPerDimension dispatch limit. When nwg.y == 1 this
47
+ // reduces to gid.x; the index >= total guard drops the tail.
48
+ let index = gid.x + gid.y * nwg.x * WG;
49
+ let total = params.rows * params.N;
50
+
51
+ if (index >= total) {
52
+ return;
53
+ }
54
+
55
+ let row = index / params.N;
56
+ let col = index % params.N;
57
+
58
+ var acc = 0.0;
59
+ let b_row_base = col * params.K;
60
+ for (var k: u32 = 0u; k < params.K; k = k + 1u) {
61
+ acc = acc + f32(a[row * params.K + k]) * dequant_weight(b_row_base + k);
62
+ }
63
+
64
+ y[index] = {{ aScalar }}(acc);
65
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.MatMulBnb4",
3
+ "id": "_com_microsoft_matmulbnb4_webgpu_0097122",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "doteNv/ofPVMbmfcGye+YHqSiBcZ65LVeDl4oDWa1Ec=",
11
+ "cast-scalar-x4.wgsl.jinja": "uplwFdE8dJSffJop4txQHZgeGDDJ5K9+F3V4n1CotVo=",
12
+ "manifest.json": "c2kZSTr7EnI+nB6vtDMF30I5Io1qYvCJwr69mQJxeD8=",
13
+ "matmul-bnb4-gemv.wgsl.jinja": "7e4ZY+raLAMG/wmXm72XpSzPsR6adHvvrcKxv2ZKj/4=",
14
+ "matmul-bnb4-sgmat.wgsl.jinja": "tm3SCGjsEM1jwLUR0W4WdjU75y8NcOSsa/1vt81YryE=",
15
+ "matmul-bnb4-tiled.wgsl.jinja": "+pb1pUgFU+RLITtJ+zrnTDl3LUmU7sZDGqIdyKtAmn4=",
16
+ "matmul-bnb4.wgsl.jinja": "xzeJm4jFqbXih7plcQup4jUQsRw4h8HpLzcAxWR2aic=",
17
+ "test.json": "1oYDh6veLTUP+o7nE+G7nU1FMK88nLKArXE/FcbOfTo="
18
+ }
19
+ },
20
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
21
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.MatMulBnb4" }
22
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.MatMulBnb4",
3
+ "fixtureArrays": {
4
+ "bnb4_weight_cycle_b_t": [1, 35, 69, 103, 137, 171, 205, 239, 254, 220, 186, 152, 118, 84, 50, 16, 90]
5
+ },
6
+ "cases": [
7
+ {
8
+ "name": "nf4_scalar_small",
9
+ "attrs": { "K": 16, "N": 4, "block_size": 16, "quant_type": 1 },
10
+ "inputs": {
11
+ "aT": {
12
+ "dtype": "float32",
13
+ "shape": [3, 16],
14
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
15
+ },
16
+ "bT": {
17
+ "dtype": "uint8",
18
+ "shape": [32],
19
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
20
+ },
21
+ "absmaxT": {
22
+ "dtype": "float32",
23
+ "shape": [4],
24
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
25
+ }
26
+ },
27
+ "outputs": { "yT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.00001 } }
28
+ },
29
+ {
30
+ "name": "fp4_scalar_small",
31
+ "attrs": { "K": 16, "N": 4, "block_size": 16, "quant_type": 0 },
32
+ "inputs": {
33
+ "aT": {
34
+ "dtype": "float32",
35
+ "shape": [3, 16],
36
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
37
+ },
38
+ "bT": {
39
+ "dtype": "uint8",
40
+ "shape": [32],
41
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
42
+ },
43
+ "absmaxT": {
44
+ "dtype": "float32",
45
+ "shape": [4],
46
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
47
+ }
48
+ },
49
+ "outputs": { "yT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.00001 } }
50
+ },
51
+ {
52
+ "name": "nf4_scalar_block_straddle",
53
+ "attrs": { "K": 24, "N": 3, "block_size": 16, "quant_type": 1 },
54
+ "inputs": {
55
+ "aT": {
56
+ "dtype": "float32",
57
+ "shape": [2, 24],
58
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
59
+ },
60
+ "bT": {
61
+ "dtype": "uint8",
62
+ "shape": [36],
63
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
64
+ },
65
+ "absmaxT": {
66
+ "dtype": "float32",
67
+ "shape": [5],
68
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
69
+ }
70
+ },
71
+ "outputs": { "yT": { "dtype": "float32", "shape": [2, 3], "tolerance": 0.00001 } }
72
+ },
73
+ {
74
+ "name": "nf4_scalar_odd_k",
75
+ "attrs": { "K": 33, "N": 5, "block_size": 16, "quant_type": 1 },
76
+ "inputs": {
77
+ "aT": {
78
+ "dtype": "float32",
79
+ "shape": [3, 33],
80
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
81
+ },
82
+ "bT": {
83
+ "dtype": "uint8",
84
+ "shape": [83],
85
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
86
+ },
87
+ "absmaxT": {
88
+ "dtype": "float32",
89
+ "shape": [11],
90
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
91
+ }
92
+ },
93
+ "outputs": { "yT": { "dtype": "float32", "shape": [3, 5], "tolerance": 0.00001 } }
94
+ },
95
+ {
96
+ "name": "nf4_gemv_decode",
97
+ "attrs": { "K": 128, "N": 64, "block_size": 64, "quant_type": 1 },
98
+ "inputs": {
99
+ "aT": {
100
+ "dtype": "float32",
101
+ "shape": [1, 128],
102
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
103
+ },
104
+ "bT": {
105
+ "dtype": "uint8",
106
+ "shape": [4096],
107
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
108
+ },
109
+ "absmaxT": {
110
+ "dtype": "float32",
111
+ "shape": [128],
112
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
113
+ }
114
+ },
115
+ "outputs": { "yT": { "dtype": "float32", "shape": [1, 64], "tolerance": 0.00002 } }
116
+ },
117
+ {
118
+ "name": "fp4_gemv_decode",
119
+ "attrs": { "K": 64, "N": 32, "block_size": 32, "quant_type": 0 },
120
+ "inputs": {
121
+ "aT": {
122
+ "dtype": "float32",
123
+ "shape": [1, 64],
124
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
125
+ },
126
+ "bT": {
127
+ "dtype": "uint8",
128
+ "shape": [1024],
129
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
130
+ },
131
+ "absmaxT": {
132
+ "dtype": "float32",
133
+ "shape": [64],
134
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
135
+ }
136
+ },
137
+ "outputs": { "yT": { "dtype": "float32", "shape": [1, 32], "tolerance": 0.00001 } }
138
+ },
139
+ {
140
+ "name": "nf4_gemv_ragged_k",
141
+ "attrs": { "K": 100, "N": 12, "block_size": 16, "quant_type": 1 },
142
+ "inputs": {
143
+ "aT": {
144
+ "dtype": "float32",
145
+ "shape": [1, 100],
146
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
147
+ },
148
+ "bT": {
149
+ "dtype": "uint8",
150
+ "shape": [600],
151
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
152
+ },
153
+ "absmaxT": {
154
+ "dtype": "float32",
155
+ "shape": [75],
156
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
157
+ }
158
+ },
159
+ "outputs": { "yT": { "dtype": "float32", "shape": [1, 12], "tolerance": 0.00001 } }
160
+ },
161
+ {
162
+ "name": "nf4_tiled_prefill",
163
+ "attrs": { "K": 64, "N": 48, "block_size": 32, "quant_type": 1 },
164
+ "inputs": {
165
+ "aT": {
166
+ "dtype": "float32",
167
+ "shape": [32, 64],
168
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
169
+ },
170
+ "bT": {
171
+ "dtype": "uint8",
172
+ "shape": [1536],
173
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
174
+ },
175
+ "absmaxT": {
176
+ "dtype": "float32",
177
+ "shape": [96],
178
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
179
+ }
180
+ },
181
+ "outputs": { "yT": { "dtype": "float32", "shape": [32, 48], "tolerance": 0.00002 } }
182
+ },
183
+ {
184
+ "name": "fp4_tiled_prefill",
185
+ "attrs": { "K": 64, "N": 48, "block_size": 32, "quant_type": 0 },
186
+ "inputs": {
187
+ "aT": {
188
+ "dtype": "float32",
189
+ "shape": [32, 64],
190
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
191
+ },
192
+ "bT": {
193
+ "dtype": "uint8",
194
+ "shape": [1536],
195
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
196
+ },
197
+ "absmaxT": {
198
+ "dtype": "float32",
199
+ "shape": [96],
200
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
201
+ }
202
+ },
203
+ "outputs": { "yT": { "dtype": "float32", "shape": [32, 48], "tolerance": 0.00002 } }
204
+ },
205
+ {
206
+ "name": "nf4_sgmat_tall_aligned",
207
+ "attrs": { "K": 64, "N": 64, "block_size": 32, "quant_type": 1 },
208
+ "inputs": {
209
+ "aT": {
210
+ "dtype": "float32",
211
+ "shape": [128, 64],
212
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
213
+ },
214
+ "bT": {
215
+ "dtype": "uint8",
216
+ "shape": [2048],
217
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
218
+ },
219
+ "absmaxT": {
220
+ "dtype": "float32",
221
+ "shape": [128],
222
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
223
+ }
224
+ },
225
+ "outputs": { "yT": { "dtype": "float32", "shape": [128, 64], "tolerance": 0.00002 } }
226
+ },
227
+ {
228
+ "name": "fp4_sgmat_aligned",
229
+ "attrs": { "K": 64, "N": 64, "block_size": 32, "quant_type": 0 },
230
+ "inputs": {
231
+ "aT": {
232
+ "dtype": "float32",
233
+ "shape": [64, 64],
234
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
235
+ },
236
+ "bT": {
237
+ "dtype": "uint8",
238
+ "shape": [2048],
239
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
240
+ },
241
+ "absmaxT": {
242
+ "dtype": "float32",
243
+ "shape": [128],
244
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
245
+ }
246
+ },
247
+ "outputs": { "yT": { "dtype": "float32", "shape": [64, 64], "tolerance": 0.00002 } }
248
+ },
249
+ {
250
+ "name": "nf4_tiled_tail",
251
+ "attrs": { "K": 33, "N": 19, "block_size": 16, "quant_type": 1 },
252
+ "inputs": {
253
+ "aT": {
254
+ "dtype": "float32",
255
+ "shape": [20, 33],
256
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
257
+ },
258
+ "bT": {
259
+ "dtype": "uint8",
260
+ "shape": [314],
261
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
262
+ },
263
+ "absmaxT": {
264
+ "dtype": "float32",
265
+ "shape": [40],
266
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
267
+ }
268
+ },
269
+ "outputs": { "yT": { "dtype": "float32", "shape": [20, 19], "tolerance": 0.00001 } }
270
+ },
271
+ {
272
+ "name": "nf4_tiled_tail_bk32",
273
+ "attrs": { "K": 33, "N": 19, "block_size": 16, "quant_type": 1 },
274
+ "tunables": { "PORTABLE_TILE_K": 32 },
275
+ "inputs": {
276
+ "aT": {
277
+ "dtype": "float32",
278
+ "shape": [20, 33],
279
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
280
+ },
281
+ "bT": {
282
+ "dtype": "uint8",
283
+ "shape": [314],
284
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
285
+ },
286
+ "absmaxT": {
287
+ "dtype": "float32",
288
+ "shape": [40],
289
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
290
+ }
291
+ },
292
+ "outputs": { "yT": { "dtype": "float32", "shape": [20, 19], "tolerance": 0.00001 } }
293
+ },
294
+ {
295
+ "name": "fp4_tiled_tail_bk64",
296
+ "attrs": { "K": 33, "N": 19, "block_size": 16, "quant_type": 0 },
297
+ "tunables": { "PORTABLE_TILE_K": 64 },
298
+ "inputs": {
299
+ "aT": {
300
+ "dtype": "float32",
301
+ "shape": [20, 33],
302
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
303
+ },
304
+ "bT": {
305
+ "dtype": "uint8",
306
+ "shape": [314],
307
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
308
+ },
309
+ "absmaxT": {
310
+ "dtype": "float32",
311
+ "shape": [40],
312
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
313
+ }
314
+ },
315
+ "outputs": { "yT": { "dtype": "float32", "shape": [20, 19], "tolerance": 0.00001 } }
316
+ },
317
+ {
318
+ "name": "nf4_f16_scalar",
319
+ "attrs": { "K": 32, "N": 8, "block_size": 16, "quant_type": 1 },
320
+ "inputs": {
321
+ "aT": {
322
+ "dtype": "float16",
323
+ "shape": [3, 32],
324
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
325
+ },
326
+ "bT": {
327
+ "dtype": "uint8",
328
+ "shape": [128],
329
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
330
+ },
331
+ "absmaxT": {
332
+ "dtype": "float16",
333
+ "shape": [16],
334
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
335
+ }
336
+ },
337
+ "outputs": { "yT": { "dtype": "float16", "shape": [3, 8], "tolerance": 0.02 } }
338
+ },
339
+ {
340
+ "name": "nf4_f16_gemv",
341
+ "attrs": { "K": 64, "N": 16, "block_size": 32, "quant_type": 1 },
342
+ "inputs": {
343
+ "aT": {
344
+ "dtype": "float16",
345
+ "shape": [1, 64],
346
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
347
+ },
348
+ "bT": {
349
+ "dtype": "uint8",
350
+ "shape": [512],
351
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
352
+ },
353
+ "absmaxT": {
354
+ "dtype": "float16",
355
+ "shape": [32],
356
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
357
+ }
358
+ },
359
+ "outputs": { "yT": { "dtype": "float16", "shape": [1, 16], "tolerance": 0.02 } }
360
+ },
361
+ {
362
+ "name": "nf4_f16_tiled",
363
+ "attrs": { "K": 32, "N": 16, "block_size": 32, "quant_type": 1 },
364
+ "inputs": {
365
+ "aT": {
366
+ "dtype": "float16",
367
+ "shape": [16, 32],
368
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
369
+ },
370
+ "bT": {
371
+ "dtype": "uint8",
372
+ "shape": [256],
373
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
374
+ },
375
+ "absmaxT": {
376
+ "dtype": "float16",
377
+ "shape": [16],
378
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
379
+ }
380
+ },
381
+ "outputs": { "yT": { "dtype": "float16", "shape": [16, 16], "tolerance": 0.02 } }
382
+ },
383
+ {
384
+ "name": "nf4_f16_tiled_longk_m32_k512_n32",
385
+ "attrs": { "K": 512, "N": 32, "block_size": 64, "quant_type": 1 },
386
+ "inputs": {
387
+ "aT": {
388
+ "dtype": "float16",
389
+ "shape": [32, 512],
390
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
391
+ },
392
+ "bT": {
393
+ "dtype": "uint8",
394
+ "shape": [8192],
395
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
396
+ },
397
+ "absmaxT": {
398
+ "dtype": "float16",
399
+ "shape": [256],
400
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
401
+ }
402
+ },
403
+ "outputs": { "yT": { "dtype": "float16", "shape": [32, 32], "tolerance": 0.001, "relTolerance": 0.001 } },
404
+ "provenance": {
405
+ "notes": "The other f16 fixtures all run K <= 64, where an f16 accumulator is indistinguishable from an f32 one. This one accumulates over K=512 so the tiled path's f32 accumulation is actually pinned."
406
+ }
407
+ },
408
+ {
409
+ "name": "nf4_sgmat_widened_f16",
410
+ "provenance": {
411
+ "notes": "A float16 request on the aligned subgroup-matrix shape. The matrix units take float32 operands and accumulate in float32, so this path widens the activations either side of the multiply rather than using the device's float16 configuration, whose result component is also float16. It is the only case that reaches the widening passes."
412
+ },
413
+ "attrs": { "K": 64, "N": 64, "block_size": 32, "quant_type": 1 },
414
+ "inputs": {
415
+ "aT": {
416
+ "dtype": "float16",
417
+ "shape": [128, 64],
418
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31 }
419
+ },
420
+ "bT": {
421
+ "dtype": "uint8",
422
+ "shape": [2048],
423
+ "data": { "kind": "cycle", "values": { "$ref": "#/fixtureArrays/bnb4_weight_cycle_b_t" } }
424
+ },
425
+ "absmaxT": {
426
+ "dtype": "float16",
427
+ "shape": [128],
428
+ "data": { "kind": "fillFloat32", "sinStep": 0.11, "cosStep": 0.07, "scale": 0.02, "offset": 0.06 }
429
+ }
430
+ },
431
+ "outputs": { "yT": { "dtype": "float16", "shape": [128, 64], "tolerance": 0.002, "relTolerance": 0.002 } }
432
+ }
433
+ ]
434
+ }