Xenova HF Staff commited on
Commit
793c2c2
·
verified ·
1 Parent(s): 07147ca

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,107 @@
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.QMoE
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Quantized mixture of experts over float32 activations and raw uint8-packed integer expert weights. The package implements 4- and 8-bit symmetric dequantization, ReLU, and interleaved SwiGLU for rank-2 or rank-3 input. Routing uses the softmax over the selected top-k logits, matching ONNX Runtime QMoE when the omitted `router_weights` input is absent. Biases, explicit zero points, FC3, sparse mixing, provider-prepacked layouts, and FP4/FP8 modes are not implemented.
16
+
17
+ See the [ONNX Runtime `QMoE` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.QMoE) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `input` | `inputT` | `T` | — | — | Token activations of shape `(num_tokens, hidden_size)` or `(batch_size, sequence_length, hidden_size)`. | required |
24
+ | `router_probs` | `routerT` | `T` | `2` | — | Routing logits of shape `(num_tokens, num_experts)`, where `num_tokens` is the product of the leading input dimensions. | required |
25
+ | `fc1_experts_weights` | `fc1T` | `T1` | `3` | — | Raw packed FC1 weights of shape `(num_experts, fusion_size * inter_size, hidden_size / pack_size)`, where `fusion_size` is 2 only for interleaved SwiGLU and `pack_size` is 2 for 4-bit or 1 for 8-bit weights. | required |
26
+ | `fc1_scales` | `fc1ScalesT` | `T2` | — | — | Required FC1 dequantization scales: rank 2 `(num_experts, fusion_size * inter_size)` for column-wise quantization, or rank 3 with a trailing `hidden_size / block_size` axis. | required |
27
+ | `fc2_experts_weights` | `fc2T` | `T1` | `3` | — | Raw packed FC2 weights of shape `(num_experts, hidden_size, inter_size / pack_size)`. | required |
28
+ | `fc2_scales` | `fc2ScalesT` | `T2` | — | — | Required FC2 dequantization scales: rank 2 `(num_experts, hidden_size)` for column-wise quantization, or rank 3 with a trailing `inter_size / block_size` axis. | required |
29
+
30
+ ## Outputs
31
+
32
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
33
+ | --- | --- | --- | --- | --- | --- | --- |
34
+ | `output` | `outputT` | `T` | same as `input` | same as `input` | Routed expert output with the same shape as `input`. | required |
35
+
36
+ ## Attributes
37
+
38
+ Attributes and default values (overridable per request):
39
+
40
+ | Attribute | Default | Description |
41
+ | --- | --- | --- |
42
+ | `activation_alpha` | `1` | Alpha used by SwiGLU; the exact standard default is 1. |
43
+ | `activation_beta` | `0` | Beta added to the SwiGLU linear branch; the exact standard default is 0. |
44
+ | `activation_type` | `"relu"` | Activation applied after FC1. This package supports `relu` and `swiglu`; the exact standard default is `relu`. |
45
+ | `expert_weight_bits` | `4` | Integer expert-weight bit width. This package supports 4 and 8; the exact standard default is 4. |
46
+ | `k` | `1` | Number of experts selected per token; the exact standard default is 1. |
47
+ | `normalize_routing_weights` | `0` | Accepted values are 0 and 1. With the separate `router_weights` input omitted, ONNX Runtime QMoE applies a softmax over the selected top-k logits for either value; the attribute only distinguishes the unsupported separate-weight path. |
48
+ | `quant_type` | `"int"` | Quantization family. This package supports only the exact standard default `int`. |
49
+ | `swiglu_fusion` | `0` | SwiGLU packing mode. ReLU uses the exact standard default 0; supported SwiGLU interleaves gate/up FC1 rows with value 1. |
50
+ | `use_sparse_mixer` | `0` | Whether to use sparse-mixer routing. The exact standard default and only supported value is 0. |
51
+ | `weights_prepacked` | `-1` | Provider weight-layout selector. Values -1 and 0 both consume the public raw packed tensor layout; provider-specific prepacked value 1 is not portable and is rejected. |
52
+ | `block_size` | — | Optional quantization block size along the reduction dimension. Omission selects column-wise scaling; a supplied value must be a power of two of at least 16 and divide both `hidden_size` and `inter_size`. |
53
+ | `swiglu_limit` | — | Optional SwiGLU clamp limit. Omission means no finite clamp. |
54
+
55
+ ## Type constraints
56
+
57
+ | Variable | Allowed dtypes |
58
+ | --- | --- |
59
+ | `T` | `float32` |
60
+ | `T1` | `uint8` |
61
+ | `T2` | `float32` |
62
+
63
+ ## Device requirements
64
+
65
+ 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.
66
+
67
+ ## Files
68
+
69
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
70
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
71
+ - [`test.json`](build/webgpu/test.json) — correctness cases
72
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
73
+ - [`expert-group-slots.wgsl.jinja`](build/webgpu/expert-group-slots.wgsl.jinja)
74
+ - [`qmoe-fc1-activation-gemv.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-gemv.wgsl.jinja)
75
+ - [`qmoe-fc1-activation-grouped-sgmat.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-grouped-sgmat.wgsl.jinja)
76
+ - [`qmoe-fc1-activation-grouped.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-grouped.wgsl.jinja)
77
+ - [`qmoe-fc1-activation-stage.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-stage.wgsl.jinja)
78
+ - [`qmoe-mix-stage.wgsl.jinja`](build/webgpu/qmoe-mix-stage.wgsl.jinja)
79
+ - [`qmoe-output-gemv.wgsl.jinja`](build/webgpu/qmoe-output-gemv.wgsl.jinja)
80
+ - [`qmoe-output-grouped-sgmat.wgsl.jinja`](build/webgpu/qmoe-output-grouped-sgmat.wgsl.jinja)
81
+ - [`qmoe-output-grouped.wgsl.jinja`](build/webgpu/qmoe-output-grouped.wgsl.jinja)
82
+ - [`qmoe-output-stage.wgsl.jinja`](build/webgpu/qmoe-output-stage.wgsl.jinja)
83
+ - [`qmoe-output-zero-inter.wgsl.jinja`](build/webgpu/qmoe-output-zero-inter.wgsl.jinja)
84
+ - [`qmoe-route-stage.wgsl.jinja`](build/webgpu/qmoe-route-stage.wgsl.jinja)
85
+
86
+ ## Use with `@huggingface/kernels`
87
+
88
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
89
+ It then allocates the result tensors automatically.
90
+
91
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
92
+
93
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
94
+
95
+ ```js
96
+ import { getKernel } from "@huggingface/kernels";
97
+
98
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.QMoE", { version: 1 });
99
+ const { outputT } = await kernel({
100
+ inputT: { data: inputTData, shape: [2, 2, 4] },
101
+ routerT: { data: routerTData, shape: [4, 2] },
102
+ fc1T: { data: fc1TData, shape: [2, 4, 2] },
103
+ fc1ScalesT: { data: fc1ScalesTData, shape: [2, 4] },
104
+ fc2T: { data: fc2TData, shape: [2, 4, 2] },
105
+ fc2ScalesT: { data: fc2ScalesTData, shape: [2, 4] },
106
+ });
107
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.QMoE",
3
+ "tunableSpace": {
4
+ "workgroupSize": [64, 128, 256],
5
+ "groupThreads": [8, 16],
6
+ "groupRegM": [1, 2, 4],
7
+ "groupRegN": [1, 2, 4],
8
+ "groupTileK": [8, 16, 32]
9
+ },
10
+ "cases": [
11
+ {
12
+ "name": "qmoe-q4-relu-t32-h64-e4-i32",
13
+ "preset": "smoke",
14
+ "vars": { "tokens": 32, "hidden": 64, "experts": 4, "inter": 32 },
15
+ "inputs": {
16
+ "inputT": { "shape": [32, 64], "dtype": "float32", "dist": "normal", "seed": 212, "scale": 0.2 },
17
+ "routerT": { "shape": [32, 4], "dtype": "float32", "dist": "normal", "seed": 213, "scale": 0.5 },
18
+ "fc1T": { "shape": [4, 32, 32], "dtype": "uint8", "dist": "uniform", "seed": 214, "min": 0, "max": 255 },
19
+ "fc1ScalesT": {
20
+ "shape": [4, 32],
21
+ "dtype": "float32",
22
+ "dist": "uniform",
23
+ "seed": 215,
24
+ "min": 0.005,
25
+ "max": 0.05
26
+ },
27
+ "fc2T": { "shape": [4, 64, 16], "dtype": "uint8", "dist": "uniform", "seed": 216, "min": 0, "max": 255 },
28
+ "fc2ScalesT": {
29
+ "shape": [4, 64],
30
+ "dtype": "float32",
31
+ "dist": "uniform",
32
+ "seed": 217,
33
+ "min": 0.005,
34
+ "max": 0.05
35
+ }
36
+ },
37
+ "outputs": { "outputT": { "shape": [32, 64], "dtype": "float32" } },
38
+ "bench": {
39
+ "primary": true,
40
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.hidden * args.inter * 2" }]
41
+ }
42
+ },
43
+ {
44
+ "name": "qmoe-q8-swiglu-t1-h128-e8-i64",
45
+ "preset": "smoke",
46
+ "vars": { "tokens": 1, "hidden": 128, "experts": 8, "inter": 64 },
47
+ "attrs": { "activation_type": "swiglu", "expert_weight_bits": 8, "swiglu_fusion": 1 },
48
+ "tunables": { "workgroupSize": 64, "decodeLanes": 32, "decodeBlockTarget": 1024 },
49
+ "tunableSpace": { "decodeMinLaneTrips": [2, 4] },
50
+ "inputs": {
51
+ "inputT": { "shape": [1, 128], "dtype": "float32", "dist": "normal", "seed": 312, "scale": 0.2 },
52
+ "routerT": { "shape": [1, 8], "dtype": "float32", "dist": "normal", "seed": 313, "scale": 0.5 },
53
+ "fc1T": { "shape": [8, 128, 128], "dtype": "uint8", "dist": "uniform", "seed": 314, "min": 0, "max": 255 },
54
+ "fc1ScalesT": {
55
+ "shape": [8, 128],
56
+ "dtype": "float32",
57
+ "dist": "uniform",
58
+ "seed": 315,
59
+ "min": 0.005,
60
+ "max": 0.05
61
+ },
62
+ "fc2T": { "shape": [8, 128, 64], "dtype": "uint8", "dist": "uniform", "seed": 316, "min": 0, "max": 255 },
63
+ "fc2ScalesT": {
64
+ "shape": [8, 128],
65
+ "dtype": "float32",
66
+ "dist": "uniform",
67
+ "seed": 317,
68
+ "min": 0.005,
69
+ "max": 0.05
70
+ }
71
+ },
72
+ "outputs": { "outputT": { "shape": [1, 128], "dtype": "float32" } },
73
+ "bench": {
74
+ "primary": true,
75
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.hidden * args.inter * 3" }]
76
+ }
77
+ },
78
+ {
79
+ "name": "qmoe-q4-swiglu-t1-h2048-e8-k2-i768-decode",
80
+ "preset": "smoke",
81
+ "vars": { "tokens": 1, "hidden": 2048, "experts": 8, "inter": 768, "topK": 2, "bits": 4 },
82
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
83
+ "tunableSpace": { "workgroupSize": [64, 128, 256], "decodeLanes": [8, 16, 32, 64] },
84
+ "inputs": {
85
+ "inputT": { "shape": [1, 2048], "dtype": "float32", "dist": "normal", "seed": 400, "scale": 0.2 },
86
+ "routerT": { "shape": [1, 8], "dtype": "float32", "dist": "normal", "seed": 401, "scale": 0.5 },
87
+ "fc1T": { "shape": [8, 1536, 1024], "dtype": "uint8", "dist": "uniform", "seed": 402, "min": 0, "max": 255 },
88
+ "fc1ScalesT": {
89
+ "shape": [8, 1536],
90
+ "dtype": "float32",
91
+ "dist": "uniform",
92
+ "seed": 403,
93
+ "min": 0.005,
94
+ "max": 0.05
95
+ },
96
+ "fc2T": { "shape": [8, 2048, 384], "dtype": "uint8", "dist": "uniform", "seed": 404, "min": 0, "max": 255 },
97
+ "fc2ScalesT": {
98
+ "shape": [8, 2048],
99
+ "dtype": "float32",
100
+ "dist": "uniform",
101
+ "seed": 405,
102
+ "min": 0.005,
103
+ "max": 0.05
104
+ }
105
+ },
106
+ "outputs": { "outputT": { "shape": [1, 2048], "dtype": "float32" } },
107
+ "bench": {
108
+ "primary": true,
109
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
110
+ },
111
+ "provenance": {
112
+ "notes": "Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
113
+ }
114
+ },
115
+ {
116
+ "name": "qmoe-q4-swiglu-t1-h1024-e16-k4-i512-decode-topk4",
117
+ "preset": "stress",
118
+ "vars": { "tokens": 1, "hidden": 1024, "experts": 16, "inter": 512, "topK": 4, "bits": 4 },
119
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 4 },
120
+ "inputs": {
121
+ "inputT": { "shape": [1, 1024], "dtype": "float32", "dist": "normal", "seed": 420, "scale": 0.2 },
122
+ "routerT": { "shape": [1, 16], "dtype": "float32", "dist": "normal", "seed": 421, "scale": 0.5 },
123
+ "fc1T": { "shape": [16, 1024, 512], "dtype": "uint8", "dist": "uniform", "seed": 422, "min": 0, "max": 255 },
124
+ "fc1ScalesT": {
125
+ "shape": [16, 1024],
126
+ "dtype": "float32",
127
+ "dist": "uniform",
128
+ "seed": 423,
129
+ "min": 0.005,
130
+ "max": 0.05
131
+ },
132
+ "fc2T": { "shape": [16, 1024, 256], "dtype": "uint8", "dist": "uniform", "seed": 424, "min": 0, "max": 255 },
133
+ "fc2ScalesT": {
134
+ "shape": [16, 1024],
135
+ "dtype": "float32",
136
+ "dist": "uniform",
137
+ "seed": 425,
138
+ "min": 0.005,
139
+ "max": 0.05
140
+ }
141
+ },
142
+ "outputs": { "outputT": { "shape": [1, 1024], "dtype": "float32" } },
143
+ "bench": {
144
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
145
+ },
146
+ "provenance": {
147
+ "notes": "Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
148
+ }
149
+ },
150
+ {
151
+ "name": "qmoe-q4-swiglu-t8-h2048-e8-k2-i768-decode-batch8",
152
+ "preset": "stress",
153
+ "vars": { "tokens": 8, "hidden": 2048, "experts": 8, "inter": 768, "topK": 2, "bits": 4 },
154
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
155
+ "tunableSpace": { "decodeBlockTarget": [128, 256] },
156
+ "inputs": {
157
+ "inputT": { "shape": [8, 2048], "dtype": "float32", "dist": "normal", "seed": 440, "scale": 0.2 },
158
+ "routerT": { "shape": [8, 8], "dtype": "float32", "dist": "normal", "seed": 441, "scale": 0.5 },
159
+ "fc1T": { "shape": [8, 1536, 1024], "dtype": "uint8", "dist": "uniform", "seed": 442, "min": 0, "max": 255 },
160
+ "fc1ScalesT": {
161
+ "shape": [8, 1536],
162
+ "dtype": "float32",
163
+ "dist": "uniform",
164
+ "seed": 443,
165
+ "min": 0.005,
166
+ "max": 0.05
167
+ },
168
+ "fc2T": { "shape": [8, 2048, 384], "dtype": "uint8", "dist": "uniform", "seed": 444, "min": 0, "max": 255 },
169
+ "fc2ScalesT": {
170
+ "shape": [8, 2048],
171
+ "dtype": "float32",
172
+ "dist": "uniform",
173
+ "seed": 445,
174
+ "min": 0.005,
175
+ "max": 0.05
176
+ }
177
+ },
178
+ "outputs": { "outputT": { "shape": [8, 2048], "dtype": "float32" } },
179
+ "bench": {
180
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
181
+ },
182
+ "provenance": {
183
+ "notes": "Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
184
+ }
185
+ },
186
+ {
187
+ "name": "qmoe-q8-swiglu-t1-h1024-e8-k2-i1024-decode-q8",
188
+ "preset": "stress",
189
+ "vars": { "tokens": 1, "hidden": 1024, "experts": 8, "inter": 1024, "topK": 2, "bits": 8 },
190
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "expert_weight_bits": 8, "k": 2 },
191
+ "inputs": {
192
+ "inputT": { "shape": [1, 1024], "dtype": "float32", "dist": "normal", "seed": 460, "scale": 0.2 },
193
+ "routerT": { "shape": [1, 8], "dtype": "float32", "dist": "normal", "seed": 461, "scale": 0.5 },
194
+ "fc1T": { "shape": [8, 2048, 1024], "dtype": "uint8", "dist": "uniform", "seed": 462, "min": 0, "max": 255 },
195
+ "fc1ScalesT": {
196
+ "shape": [8, 2048],
197
+ "dtype": "float32",
198
+ "dist": "uniform",
199
+ "seed": 463,
200
+ "min": 0.005,
201
+ "max": 0.05
202
+ },
203
+ "fc2T": { "shape": [8, 1024, 1024], "dtype": "uint8", "dist": "uniform", "seed": 464, "min": 0, "max": 255 },
204
+ "fc2ScalesT": {
205
+ "shape": [8, 1024],
206
+ "dtype": "float32",
207
+ "dist": "uniform",
208
+ "seed": 465,
209
+ "min": 0.005,
210
+ "max": 0.05
211
+ }
212
+ },
213
+ "outputs": { "outputT": { "shape": [1, 1024], "dtype": "float32" } },
214
+ "bench": {
215
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
216
+ },
217
+ "provenance": {
218
+ "notes": "Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
219
+ }
220
+ },
221
+ {
222
+ "name": "qmoe-q4-relu-t1-h2048-e8-i2048-decode-relu",
223
+ "preset": "stress",
224
+ "vars": { "tokens": 1, "hidden": 2048, "experts": 8, "inter": 2048, "topK": 1, "bits": 4 },
225
+ "inputs": {
226
+ "inputT": { "shape": [1, 2048], "dtype": "float32", "dist": "normal", "seed": 480, "scale": 0.2 },
227
+ "routerT": { "shape": [1, 8], "dtype": "float32", "dist": "normal", "seed": 481, "scale": 0.5 },
228
+ "fc1T": { "shape": [8, 2048, 1024], "dtype": "uint8", "dist": "uniform", "seed": 482, "min": 0, "max": 255 },
229
+ "fc1ScalesT": {
230
+ "shape": [8, 2048],
231
+ "dtype": "float32",
232
+ "dist": "uniform",
233
+ "seed": 483,
234
+ "min": 0.005,
235
+ "max": 0.05
236
+ },
237
+ "fc2T": { "shape": [8, 2048, 1024], "dtype": "uint8", "dist": "uniform", "seed": 484, "min": 0, "max": 255 },
238
+ "fc2ScalesT": {
239
+ "shape": [8, 2048],
240
+ "dtype": "float32",
241
+ "dist": "uniform",
242
+ "seed": 485,
243
+ "min": 0.005,
244
+ "max": 0.05
245
+ }
246
+ },
247
+ "outputs": { "outputT": { "shape": [1, 2048], "dtype": "float32" } },
248
+ "bench": {
249
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 2 * args.hidden * args.inter * args.bits / 2" }]
250
+ },
251
+ "provenance": {
252
+ "notes": "Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
253
+ }
254
+ },
255
+ {
256
+ "name": "qmoe-q4-swiglu-t48-h1024-e8-k2-i1024-prefill-band",
257
+ "preset": "stress",
258
+ "vars": { "tokens": 48, "hidden": 1024, "experts": 8, "inter": 1024, "topK": 2 },
259
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
260
+ "inputs": {
261
+ "inputT": { "shape": [48, 1024], "dtype": "float32", "dist": "normal", "seed": 600, "scale": 0.2 },
262
+ "routerT": { "shape": [48, 8], "dtype": "float32", "dist": "normal", "seed": 601, "scale": 0.5 },
263
+ "fc1T": { "shape": [8, 2048, 512], "dtype": "uint8", "dist": "uniform", "seed": 502, "min": 0, "max": 255 },
264
+ "fc1ScalesT": {
265
+ "shape": [8, 2048],
266
+ "dtype": "float32",
267
+ "dist": "uniform",
268
+ "seed": 503,
269
+ "min": 0.005,
270
+ "max": 0.05
271
+ },
272
+ "fc2T": { "shape": [8, 1024, 512], "dtype": "uint8", "dist": "uniform", "seed": 504, "min": 0, "max": 255 },
273
+ "fc2ScalesT": {
274
+ "shape": [8, 1024],
275
+ "dtype": "float32",
276
+ "dist": "uniform",
277
+ "seed": 505,
278
+ "min": 0.005,
279
+ "max": 0.05
280
+ }
281
+ },
282
+ "outputs": { "outputT": { "shape": [48, 1024], "dtype": "float32" } },
283
+ "bench": {
284
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
285
+ }
286
+ },
287
+ {
288
+ "name": "qmoe-q4-swiglu-t96-h1024-e8-k2-i1024-prefill-small",
289
+ "preset": "stress",
290
+ "vars": { "tokens": 96, "hidden": 1024, "experts": 8, "inter": 1024, "topK": 2 },
291
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
292
+ "tunables": { "workgroupSize": 128, "decodeLanes": 32, "decodeMinLaneTrips": 4 },
293
+ "inputs": {
294
+ "inputT": { "shape": [96, 1024], "dtype": "float32", "dist": "normal", "seed": 620, "scale": 0.2 },
295
+ "routerT": { "shape": [96, 8], "dtype": "float32", "dist": "normal", "seed": 621, "scale": 0.5 },
296
+ "fc1T": { "shape": [8, 2048, 512], "dtype": "uint8", "dist": "uniform", "seed": 502, "min": 0, "max": 255 },
297
+ "fc1ScalesT": {
298
+ "shape": [8, 2048],
299
+ "dtype": "float32",
300
+ "dist": "uniform",
301
+ "seed": 503,
302
+ "min": 0.005,
303
+ "max": 0.05
304
+ },
305
+ "fc2T": { "shape": [8, 1024, 512], "dtype": "uint8", "dist": "uniform", "seed": 504, "min": 0, "max": 255 },
306
+ "fc2ScalesT": {
307
+ "shape": [8, 1024],
308
+ "dtype": "float32",
309
+ "dist": "uniform",
310
+ "seed": 505,
311
+ "min": 0.005,
312
+ "max": 0.05
313
+ }
314
+ },
315
+ "outputs": { "outputT": { "shape": [96, 1024], "dtype": "float32" } },
316
+ "bench": {
317
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
318
+ }
319
+ },
320
+ {
321
+ "name": "qmoe-q4-swiglu-t128-h1024-e8-k2-i1024-prefill",
322
+ "preset": "stress",
323
+ "vars": { "tokens": 128, "hidden": 1024, "experts": 8, "inter": 1024, "topK": 2 },
324
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
325
+ "tunables": { "workgroupSize": 128, "decodeLanes": 32, "decodeMinLaneTrips": 4 },
326
+ "tunableSpace": { "groupRegM": [2, 4], "groupRegN": [2, 4], "groupTileK": [8, 16] },
327
+ "inputs": {
328
+ "inputT": { "shape": [128, 1024], "dtype": "float32", "dist": "normal", "seed": 500, "scale": 0.2 },
329
+ "routerT": { "shape": [128, 8], "dtype": "float32", "dist": "normal", "seed": 501, "scale": 0.5 },
330
+ "fc1T": { "shape": [8, 2048, 512], "dtype": "uint8", "dist": "uniform", "seed": 502, "min": 0, "max": 255 },
331
+ "fc1ScalesT": {
332
+ "shape": [8, 2048],
333
+ "dtype": "float32",
334
+ "dist": "uniform",
335
+ "seed": 503,
336
+ "min": 0.005,
337
+ "max": 0.05
338
+ },
339
+ "fc2T": { "shape": [8, 1024, 512], "dtype": "uint8", "dist": "uniform", "seed": 504, "min": 0, "max": 255 },
340
+ "fc2ScalesT": {
341
+ "shape": [8, 1024],
342
+ "dtype": "float32",
343
+ "dist": "uniform",
344
+ "seed": 505,
345
+ "min": 0.005,
346
+ "max": 0.05
347
+ }
348
+ },
349
+ "outputs": { "outputT": { "shape": [128, 1024], "dtype": "float32" } },
350
+ "bench": {
351
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
352
+ }
353
+ },
354
+ {
355
+ "name": "qmoe-q4-swiglu-t512-h1024-e8-k2-i1024-prefill-long",
356
+ "preset": "stress",
357
+ "vars": { "tokens": 512, "hidden": 1024, "experts": 8, "inter": 1024, "topK": 2 },
358
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
359
+ "inputs": {
360
+ "inputT": { "shape": [512, 1024], "dtype": "float32", "dist": "normal", "seed": 520, "scale": 0.2 },
361
+ "routerT": { "shape": [512, 8], "dtype": "float32", "dist": "normal", "seed": 521, "scale": 0.5 },
362
+ "fc1T": { "shape": [8, 2048, 512], "dtype": "uint8", "dist": "uniform", "seed": 522, "min": 0, "max": 255 },
363
+ "fc1ScalesT": {
364
+ "shape": [8, 2048],
365
+ "dtype": "float32",
366
+ "dist": "uniform",
367
+ "seed": 523,
368
+ "min": 0.005,
369
+ "max": 0.05
370
+ },
371
+ "fc2T": { "shape": [8, 1024, 512], "dtype": "uint8", "dist": "uniform", "seed": 524, "min": 0, "max": 255 },
372
+ "fc2ScalesT": {
373
+ "shape": [8, 1024],
374
+ "dtype": "float32",
375
+ "dist": "uniform",
376
+ "seed": 525,
377
+ "min": 0.005,
378
+ "max": 0.05
379
+ }
380
+ },
381
+ "outputs": { "outputT": { "shape": [512, 1024], "dtype": "float32" } },
382
+ "bench": {
383
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
384
+ }
385
+ },
386
+ {
387
+ "name": "qmoe-q4-swiglu-t384-h1024-e32-k1-i512-prefill-band",
388
+ "preset": "stress",
389
+ "vars": { "tokens": 384, "hidden": 1024, "experts": 32, "inter": 512, "topK": 1 },
390
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 1 },
391
+ "inputs": {
392
+ "inputT": { "shape": [384, 1024], "dtype": "float32", "dist": "normal", "seed": 700, "scale": 0.2 },
393
+ "routerT": { "shape": [384, 32], "dtype": "float32", "dist": "normal", "seed": 701, "scale": 0.5 },
394
+ "fc1T": { "shape": [32, 1024, 512], "dtype": "uint8", "dist": "uniform", "seed": 702, "min": 0, "max": 255 },
395
+ "fc1ScalesT": {
396
+ "shape": [32, 1024],
397
+ "dtype": "float32",
398
+ "dist": "uniform",
399
+ "seed": 703,
400
+ "min": 0.005,
401
+ "max": 0.05
402
+ },
403
+ "fc2T": { "shape": [32, 1024, 256], "dtype": "uint8", "dist": "uniform", "seed": 704, "min": 0, "max": 255 },
404
+ "fc2ScalesT": {
405
+ "shape": [32, 1024],
406
+ "dtype": "float32",
407
+ "dist": "uniform",
408
+ "seed": 705,
409
+ "min": 0.005,
410
+ "max": 0.05
411
+ }
412
+ },
413
+ "outputs": { "outputT": { "shape": [384, 1024], "dtype": "float32" } },
414
+ "bench": {
415
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
416
+ }
417
+ },
418
+ {
419
+ "name": "qmoe-q4-qwen3-moe-decode-t1-h2048-e64-k8-i768",
420
+ "preset": "model",
421
+ "provenance": {
422
+ "notes": "Qwen3-MoE class defaults (hidden_size 2048, moe_intermediate_size 768, num_experts_per_tok 8); expert count capped at 64 of the config's 128 to keep each weight tensor under 512 MB. Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
423
+ },
424
+ "vars": { "tokens": 1, "hidden": 2048, "experts": 64, "inter": 768, "topK": 8, "bits": 4 },
425
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 8 },
426
+ "inputs": {
427
+ "inputT": { "shape": [1, 2048], "dtype": "float32", "dist": "normal", "seed": 4011, "scale": 0.2 },
428
+ "routerT": { "shape": [1, 64], "dtype": "float32", "dist": "normal", "seed": 4012, "scale": 0.5 },
429
+ "fc1T": { "shape": [64, 1536, 1024], "dtype": "uint8", "dist": "uniform", "seed": 4013, "min": 0, "max": 255 },
430
+ "fc1ScalesT": {
431
+ "shape": [64, 1536],
432
+ "dtype": "float32",
433
+ "dist": "uniform",
434
+ "seed": 4014,
435
+ "min": 0.005,
436
+ "max": 0.05
437
+ },
438
+ "fc2T": { "shape": [64, 2048, 384], "dtype": "uint8", "dist": "uniform", "seed": 4015, "min": 0, "max": 255 },
439
+ "fc2ScalesT": {
440
+ "shape": [64, 2048],
441
+ "dtype": "float32",
442
+ "dist": "uniform",
443
+ "seed": 4016,
444
+ "min": 0.005,
445
+ "max": 0.05
446
+ }
447
+ },
448
+ "outputs": { "outputT": { "shape": [1, 2048], "dtype": "float32" } },
449
+ "bench": {
450
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
451
+ }
452
+ },
453
+ {
454
+ "name": "qmoe-q4-qwen3-moe-prefill-t256-h2048-e64-k8-i768",
455
+ "preset": "model",
456
+ "provenance": {
457
+ "notes": "Qwen3-MoE class defaults at a 256-token prefill chunk; 2048 routed slots clear the grouped schedule's slot floor for 64 experts."
458
+ },
459
+ "vars": { "tokens": 256, "hidden": 2048, "experts": 64, "inter": 768, "topK": 8 },
460
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 8 },
461
+ "inputs": {
462
+ "inputT": { "shape": [256, 2048], "dtype": "float32", "dist": "normal", "seed": 4021, "scale": 0.2 },
463
+ "routerT": { "shape": [256, 64], "dtype": "float32", "dist": "normal", "seed": 4022, "scale": 0.5 },
464
+ "fc1T": { "shape": [64, 1536, 1024], "dtype": "uint8", "dist": "uniform", "seed": 4023, "min": 0, "max": 255 },
465
+ "fc1ScalesT": {
466
+ "shape": [64, 1536],
467
+ "dtype": "float32",
468
+ "dist": "uniform",
469
+ "seed": 4024,
470
+ "min": 0.005,
471
+ "max": 0.05
472
+ },
473
+ "fc2T": { "shape": [64, 2048, 384], "dtype": "uint8", "dist": "uniform", "seed": 4025, "min": 0, "max": 255 },
474
+ "fc2ScalesT": {
475
+ "shape": [64, 2048],
476
+ "dtype": "float32",
477
+ "dist": "uniform",
478
+ "seed": 4026,
479
+ "min": 0.005,
480
+ "max": 0.05
481
+ }
482
+ },
483
+ "outputs": { "outputT": { "shape": [256, 2048], "dtype": "float32" } },
484
+ "bench": {
485
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
486
+ }
487
+ },
488
+ {
489
+ "name": "qmoe-q4-olmoe-decode-t1-h2048-e32-k8-i2048",
490
+ "preset": "model",
491
+ "provenance": {
492
+ "notes": "OLMoE class defaults (hidden_size 2048, intermediate_size 2048, num_experts_per_tok 8); expert count capped at 32 of the config's 64. Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
493
+ },
494
+ "vars": { "tokens": 1, "hidden": 2048, "experts": 32, "inter": 2048, "topK": 8, "bits": 4 },
495
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 8 },
496
+ "inputs": {
497
+ "inputT": { "shape": [1, 2048], "dtype": "float32", "dist": "normal", "seed": 4031, "scale": 0.2 },
498
+ "routerT": { "shape": [1, 32], "dtype": "float32", "dist": "normal", "seed": 4032, "scale": 0.5 },
499
+ "fc1T": { "shape": [32, 4096, 1024], "dtype": "uint8", "dist": "uniform", "seed": 4033, "min": 0, "max": 255 },
500
+ "fc1ScalesT": {
501
+ "shape": [32, 4096],
502
+ "dtype": "float32",
503
+ "dist": "uniform",
504
+ "seed": 4034,
505
+ "min": 0.005,
506
+ "max": 0.05
507
+ },
508
+ "fc2T": { "shape": [32, 2048, 1024], "dtype": "uint8", "dist": "uniform", "seed": 4035, "min": 0, "max": 255 },
509
+ "fc2ScalesT": {
510
+ "shape": [32, 2048],
511
+ "dtype": "float32",
512
+ "dist": "uniform",
513
+ "seed": 4036,
514
+ "min": 0.005,
515
+ "max": 0.05
516
+ }
517
+ },
518
+ "outputs": { "outputT": { "shape": [1, 2048], "dtype": "float32" } },
519
+ "bench": {
520
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
521
+ }
522
+ },
523
+ {
524
+ "name": "qmoe-q4-olmoe-prefill-t128-h2048-e32-k8-i2048",
525
+ "preset": "model",
526
+ "provenance": { "notes": "OLMoE class defaults at a 128-token prefill chunk." },
527
+ "vars": { "tokens": 128, "hidden": 2048, "experts": 32, "inter": 2048, "topK": 8 },
528
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 8 },
529
+ "inputs": {
530
+ "inputT": { "shape": [128, 2048], "dtype": "float32", "dist": "normal", "seed": 4041, "scale": 0.2 },
531
+ "routerT": { "shape": [128, 32], "dtype": "float32", "dist": "normal", "seed": 4042, "scale": 0.5 },
532
+ "fc1T": { "shape": [32, 4096, 1024], "dtype": "uint8", "dist": "uniform", "seed": 4043, "min": 0, "max": 255 },
533
+ "fc1ScalesT": {
534
+ "shape": [32, 4096],
535
+ "dtype": "float32",
536
+ "dist": "uniform",
537
+ "seed": 4044,
538
+ "min": 0.005,
539
+ "max": 0.05
540
+ },
541
+ "fc2T": { "shape": [32, 2048, 1024], "dtype": "uint8", "dist": "uniform", "seed": 4045, "min": 0, "max": 255 },
542
+ "fc2ScalesT": {
543
+ "shape": [32, 2048],
544
+ "dtype": "float32",
545
+ "dist": "uniform",
546
+ "seed": 4046,
547
+ "min": 0.005,
548
+ "max": 0.05
549
+ }
550
+ },
551
+ "outputs": { "outputT": { "shape": [128, 2048], "dtype": "float32" } },
552
+ "bench": {
553
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
554
+ }
555
+ },
556
+ {
557
+ "name": "qmoe-q4-gpt-oss-decode-t1-h2880-e16-k4-i2880",
558
+ "preset": "model",
559
+ "provenance": {
560
+ "notes": "GPT-OSS class defaults (hidden_size 2880, intermediate_size 2880, num_experts_per_tok 4); expert count capped at 16 of the config's 128. Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
561
+ },
562
+ "vars": { "tokens": 1, "hidden": 2880, "experts": 16, "inter": 2880, "topK": 4, "bits": 4 },
563
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 4 },
564
+ "inputs": {
565
+ "inputT": { "shape": [1, 2880], "dtype": "float32", "dist": "normal", "seed": 4051, "scale": 0.2 },
566
+ "routerT": { "shape": [1, 16], "dtype": "float32", "dist": "normal", "seed": 4052, "scale": 0.5 },
567
+ "fc1T": { "shape": [16, 5760, 1440], "dtype": "uint8", "dist": "uniform", "seed": 4053, "min": 0, "max": 255 },
568
+ "fc1ScalesT": {
569
+ "shape": [16, 5760],
570
+ "dtype": "float32",
571
+ "dist": "uniform",
572
+ "seed": 4054,
573
+ "min": 0.005,
574
+ "max": 0.05
575
+ },
576
+ "fc2T": { "shape": [16, 2880, 1440], "dtype": "uint8", "dist": "uniform", "seed": 4055, "min": 0, "max": 255 },
577
+ "fc2ScalesT": {
578
+ "shape": [16, 2880],
579
+ "dtype": "float32",
580
+ "dist": "uniform",
581
+ "seed": 4056,
582
+ "min": 0.005,
583
+ "max": 0.05
584
+ }
585
+ },
586
+ "outputs": { "outputT": { "shape": [1, 2880], "dtype": "float32" } },
587
+ "bench": {
588
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
589
+ }
590
+ },
591
+ {
592
+ "name": "qmoe-q4-gpt-oss-prefill-t256-h2880-e16-k4-i2880",
593
+ "preset": "model",
594
+ "provenance": { "notes": "GPT-OSS class defaults at a 256-token prefill chunk." },
595
+ "vars": { "tokens": 256, "hidden": 2880, "experts": 16, "inter": 2880, "topK": 4 },
596
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 4 },
597
+ "inputs": {
598
+ "inputT": { "shape": [256, 2880], "dtype": "float32", "dist": "normal", "seed": 4061, "scale": 0.2 },
599
+ "routerT": { "shape": [256, 16], "dtype": "float32", "dist": "normal", "seed": 4062, "scale": 0.5 },
600
+ "fc1T": { "shape": [16, 5760, 1440], "dtype": "uint8", "dist": "uniform", "seed": 4063, "min": 0, "max": 255 },
601
+ "fc1ScalesT": {
602
+ "shape": [16, 5760],
603
+ "dtype": "float32",
604
+ "dist": "uniform",
605
+ "seed": 4064,
606
+ "min": 0.005,
607
+ "max": 0.05
608
+ },
609
+ "fc2T": { "shape": [16, 2880, 1440], "dtype": "uint8", "dist": "uniform", "seed": 4065, "min": 0, "max": 255 },
610
+ "fc2ScalesT": {
611
+ "shape": [16, 2880],
612
+ "dtype": "float32",
613
+ "dist": "uniform",
614
+ "seed": 4066,
615
+ "min": 0.005,
616
+ "max": 0.05
617
+ }
618
+ },
619
+ "outputs": { "outputT": { "shape": [256, 2880], "dtype": "float32" } },
620
+ "bench": {
621
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
622
+ }
623
+ },
624
+ {
625
+ "name": "qmoe-q4-phimoe-decode-t1-h4096-e4-k2-i6400",
626
+ "preset": "model",
627
+ "provenance": {
628
+ "notes": "Phi-MoE class defaults (hidden_size 4096, intermediate_size 6400, num_experts_per_tok 2); expert count capped at 4 of the config's 16 -- the widest expert in this set, so it is the case that stresses the reduction rather than the routing. Scored as bandwidth, not gflops: a decode step reads whole expert weight matrices to produce one output row, so memory is its roofline and the compute peak is the wrong denominator. The prefill cases stay on gflops. The bytes are GPU storage, not the packed blob: a uint8 tensor takes one byte per u32 element here, so a b-bit code costs b/2 bytes of traffic."
629
+ },
630
+ "vars": { "tokens": 1, "hidden": 4096, "experts": 4, "inter": 6400, "topK": 2, "bits": 4 },
631
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
632
+ "inputs": {
633
+ "inputT": { "shape": [1, 4096], "dtype": "float32", "dist": "normal", "seed": 4071, "scale": 0.2 },
634
+ "routerT": { "shape": [1, 4], "dtype": "float32", "dist": "normal", "seed": 4072, "scale": 0.5 },
635
+ "fc1T": { "shape": [4, 12800, 2048], "dtype": "uint8", "dist": "uniform", "seed": 4073, "min": 0, "max": 255 },
636
+ "fc1ScalesT": {
637
+ "shape": [4, 12800],
638
+ "dtype": "float32",
639
+ "dist": "uniform",
640
+ "seed": 4074,
641
+ "min": 0.005,
642
+ "max": 0.05
643
+ },
644
+ "fc2T": { "shape": [4, 4096, 3200], "dtype": "uint8", "dist": "uniform", "seed": 4075, "min": 0, "max": 255 },
645
+ "fc2ScalesT": {
646
+ "shape": [4, 4096],
647
+ "dtype": "float32",
648
+ "dist": "uniform",
649
+ "seed": 4076,
650
+ "min": 0.005,
651
+ "max": 0.05
652
+ }
653
+ },
654
+ "outputs": { "outputT": { "shape": [1, 4096], "dtype": "float32" } },
655
+ "bench": {
656
+ "metrics": [{ "type": "bandwidth", "value": "args.topK * 3 * args.hidden * args.inter * args.bits / 2" }]
657
+ }
658
+ },
659
+ {
660
+ "name": "qmoe-q4-phimoe-prefill-t256-h4096-e4-k2-i6400",
661
+ "preset": "model",
662
+ "provenance": { "notes": "Phi-MoE class defaults at a 256-token prefill chunk." },
663
+ "vars": { "tokens": 256, "hidden": 4096, "experts": 4, "inter": 6400, "topK": 2 },
664
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1, "k": 2 },
665
+ "inputs": {
666
+ "inputT": { "shape": [256, 4096], "dtype": "float32", "dist": "normal", "seed": 4081, "scale": 0.2 },
667
+ "routerT": { "shape": [256, 4], "dtype": "float32", "dist": "normal", "seed": 4082, "scale": 0.5 },
668
+ "fc1T": { "shape": [4, 12800, 2048], "dtype": "uint8", "dist": "uniform", "seed": 4083, "min": 0, "max": 255 },
669
+ "fc1ScalesT": {
670
+ "shape": [4, 12800],
671
+ "dtype": "float32",
672
+ "dist": "uniform",
673
+ "seed": 4084,
674
+ "min": 0.005,
675
+ "max": 0.05
676
+ },
677
+ "fc2T": { "shape": [4, 4096, 3200], "dtype": "uint8", "dist": "uniform", "seed": 4085, "min": 0, "max": 255 },
678
+ "fc2ScalesT": {
679
+ "shape": [4, 4096],
680
+ "dtype": "float32",
681
+ "dist": "uniform",
682
+ "seed": 4086,
683
+ "min": 0.005,
684
+ "max": 0.05
685
+ }
686
+ },
687
+ "outputs": { "outputT": { "shape": [256, 4096], "dtype": "float32" } },
688
+ "bench": {
689
+ "metrics": [{ "type": "gflops", "value": "2 * args.tokens * args.topK * args.hidden * args.inter * 3" }]
690
+ }
691
+ }
692
+ ]
693
+ }
build/webgpu/expert-group-slots.wgsl.jinja ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Group this token chunk's routed slots by expert, then cut each expert's run into
4
+ // MTILE-row tiles for the grouped FFN stage.
5
+ //
6
+ // The unit being grouped is the SLOT, not the token: a token contributes TOP_K slots and each
7
+ // one carries its own expert, so the slot space is a K-COVER of [0, chunkTokens * TOP_K) rather
8
+ // than a partition of the tokens. That is what lets the grouped tiles serve any k -- a token
9
+ // simply appears once in each of the (up to TOP_K) expert runs it was routed to.
10
+ //
11
+ // tile_meta[0] is the tile count; tile t occupies the three words at 1 + t * 3 and holds its
12
+ // expert, the start of its slice of slot_list, and how many rows of that slice are real.
13
+ //
14
+ // Each expert run may end with one partial tile, so the grouped schedule carries
15
+ // about EXPERTS * MTILE / 2 padded rows.
16
+ const EXPERTS: u32 = {{ experts }}u;
17
+ const TOP_K: u32 = {{ topK }}u;
18
+ const MTILE: u32 = {{ groupTileM }}u;
19
+ const WG: u32 = {{ groupRouteWorkgroup }}u;
20
+
21
+ var<workgroup> counts: array<atomic<u32>, {{ experts }}>;
22
+ var<workgroup> offsets: array<u32, {{ experts }}>;
23
+
24
+ @compute @workgroup_size(WG, 1, 1)
25
+ fn main(@builtin(local_invocation_index) lid: u32) {
26
+ let slot_total = params.tokenCount * TOP_K;
27
+ let slot_base = params.tokenOffset * TOP_K;
28
+
29
+ for (var e = lid; e < EXPERTS; e = e + WG) {
30
+ atomicStore(&counts[e], 0u);
31
+ }
32
+ workgroupBarrier();
33
+
34
+ for (var p = lid; p < slot_total; p = p + WG) {
35
+ atomicAdd(&counts[route_expert[slot_base + p]], 1u);
36
+ }
37
+ workgroupBarrier();
38
+
39
+ // One lane lays out the EXPERTS-sized run and tile tables. The chunk-sized
40
+ // scatter below is distributed over the workgroup.
41
+ if (lid == 0u) {
42
+ var offset = 0u;
43
+ var tile = 0u;
44
+ for (var e = 0u; e < EXPERTS; e = e + 1u) {
45
+ offsets[e] = offset;
46
+ let n = atomicLoad(&counts[e]);
47
+ var done = 0u;
48
+ while (done < n) {
49
+ let rows = min(MTILE, n - done);
50
+ tile_meta[1u + tile * 3u] = e;
51
+ tile_meta[1u + tile * 3u + 1u] = offset + done;
52
+ tile_meta[1u + tile * 3u + 2u] = rows;
53
+ tile = tile + 1u;
54
+ done = done + rows;
55
+ }
56
+ offset = offset + n;
57
+ atomicStore(&counts[e], 0u);
58
+ }
59
+ tile_meta[0] = tile;
60
+ }
61
+ workgroupBarrier();
62
+
63
+ // Each slot atomically claims a position in its expert's run. Arrival order is
64
+ // safe because downstream dots use the slot's own row and reduce to its token;
65
+ // no consumer assigns meaning to order within a run.
66
+ for (var p = lid; p < slot_total; p = p + WG) {
67
+ let expert = route_expert[slot_base + p];
68
+ slot_list[offsets[expert] + atomicAdd(&counts[expert], 1u)] = p;
69
+ }
70
+ }
build/webgpu/manifest.json ADDED
@@ -0,0 +1,813 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "domain": "com.microsoft",
3
+ "name": "QMoE",
4
+ "sinceVersion": 1,
5
+ "description": "Quantized mixture of experts over float32 activations and raw uint8-packed integer expert weights. The package implements 4- and 8-bit symmetric dequantization, ReLU, and interleaved SwiGLU for rank-2 or rank-3 input. Routing uses the softmax over the selected top-k logits, matching ONNX Runtime QMoE when the omitted `router_weights` input is absent. Biases, explicit zero points, FC3, sparse mixing, provider-prepacked layouts, and FP4/FP8 modes are not implemented.",
6
+ "inputs": [
7
+ {
8
+ "role": "input",
9
+ "dtype": "T",
10
+ "description": "Token activations of shape `(num_tokens, hidden_size)` or `(batch_size, sequence_length, hidden_size)`."
11
+ },
12
+ {
13
+ "role": "router_probs",
14
+ "dtype": "T",
15
+ "rank": 2,
16
+ "description": "Routing logits of shape `(num_tokens, num_experts)`, where `num_tokens` is the product of the leading input dimensions."
17
+ },
18
+ {
19
+ "role": "fc1_experts_weights",
20
+ "dtype": "T1",
21
+ "rank": 3,
22
+ "description": "Raw packed FC1 weights of shape `(num_experts, fusion_size * inter_size, hidden_size / pack_size)`, where `fusion_size` is 2 only for interleaved SwiGLU and `pack_size` is 2 for 4-bit or 1 for 8-bit weights."
23
+ },
24
+ {
25
+ "role": "fc1_scales",
26
+ "dtype": "T2",
27
+ "description": "Required FC1 dequantization scales: rank 2 `(num_experts, fusion_size * inter_size)` for column-wise quantization, or rank 3 with a trailing `hidden_size / block_size` axis."
28
+ },
29
+ {
30
+ "role": "fc2_experts_weights",
31
+ "dtype": "T1",
32
+ "rank": 3,
33
+ "description": "Raw packed FC2 weights of shape `(num_experts, hidden_size, inter_size / pack_size)`."
34
+ },
35
+ {
36
+ "role": "fc2_scales",
37
+ "dtype": "T2",
38
+ "description": "Required FC2 dequantization scales: rank 2 `(num_experts, hidden_size)` for column-wise quantization, or rank 3 with a trailing `inter_size / block_size` axis."
39
+ }
40
+ ],
41
+ "outputs": [
42
+ {
43
+ "role": "output",
44
+ "dtype": "T",
45
+ "shape": "shapes.input",
46
+ "description": "Routed expert output with the same shape as `input`."
47
+ }
48
+ ],
49
+ "attributes": {
50
+ "activation_alpha": 1,
51
+ "activation_beta": 0,
52
+ "activation_type": "relu",
53
+ "expert_weight_bits": 4,
54
+ "k": 1,
55
+ "normalize_routing_weights": 0,
56
+ "quant_type": "int",
57
+ "swiglu_fusion": 0,
58
+ "use_sparse_mixer": 0,
59
+ "weights_prepacked": -1
60
+ },
61
+ "attributeDescriptions": {
62
+ "activation_alpha": "Alpha used by SwiGLU; the exact standard default is 1.",
63
+ "activation_beta": "Beta added to the SwiGLU linear branch; the exact standard default is 0.",
64
+ "activation_type": "Activation applied after FC1. This package supports `relu` and `swiglu`; the exact standard default is `relu`.",
65
+ "block_size": "Optional quantization block size along the reduction dimension. Omission selects column-wise scaling; a supplied value must be a power of two of at least 16 and divide both `hidden_size` and `inter_size`.",
66
+ "expert_weight_bits": "Integer expert-weight bit width. This package supports 4 and 8; the exact standard default is 4.",
67
+ "k": "Number of experts selected per token; the exact standard default is 1.",
68
+ "normalize_routing_weights": "Accepted values are 0 and 1. With the separate `router_weights` input omitted, ONNX Runtime QMoE applies a softmax over the selected top-k logits for either value; the attribute only distinguishes the unsupported separate-weight path.",
69
+ "quant_type": "Quantization family. This package supports only the exact standard default `int`.",
70
+ "swiglu_fusion": "SwiGLU packing mode. ReLU uses the exact standard default 0; supported SwiGLU interleaves gate/up FC1 rows with value 1.",
71
+ "swiglu_limit": "Optional SwiGLU clamp limit. Omission means no finite clamp.",
72
+ "use_sparse_mixer": "Whether to use sparse-mixer routing. The exact standard default and only supported value is 0.",
73
+ "weights_prepacked": "Provider weight-layout selector. Values -1 and 0 both consume the public raw packed tensor layout; provider-specific prepacked value 1 is not portable and is rejected."
74
+ },
75
+ "attributeConstraints": {
76
+ "activation_type": { "values": ["relu", "swiglu"] },
77
+ "expert_weight_bits": { "values": [4, 8] },
78
+ "normalize_routing_weights": { "values": [0, 1] },
79
+ "quant_type": { "values": ["int"] },
80
+ "swiglu_fusion": { "values": [0, 1] },
81
+ "use_sparse_mixer": { "values": [0] },
82
+ "weights_prepacked": { "values": [-1, 0] }
83
+ },
84
+ "typeConstraints": { "T": ["float32"], "T1": ["uint8"], "T2": ["float32"] },
85
+ "args": {
86
+ "inputT": { "kind": "tensor", "semantic": "input", "role": "input" },
87
+ "routerT": { "kind": "tensor", "semantic": "router_probs", "role": "input" },
88
+ "fc1T": { "kind": "tensor", "semantic": "fc1_experts_weights", "role": "input" },
89
+ "fc1ScalesT": { "kind": "tensor", "semantic": "fc1_scales", "role": "input" },
90
+ "fc2T": { "kind": "tensor", "semantic": "fc2_experts_weights", "role": "input" },
91
+ "fc2ScalesT": { "kind": "tensor", "semantic": "fc2_scales", "role": "input" },
92
+ "outputT": { "kind": "tensor", "semantic": "output", "role": "output" }
93
+ },
94
+ "tunables": {
95
+ "workgroupSize": 64,
96
+ "decodeLanes": 32,
97
+ "decodeBlockTarget": 1024,
98
+ "decodeMinLaneTrips": 4,
99
+ "groupThreads": 8,
100
+ "groupRegM": 4,
101
+ "groupRegN": 4,
102
+ "groupTileK": 16,
103
+ "groupRouteWorkgroup": 256
104
+ },
105
+ "derive": {
106
+ "deviceWorkgroupCap": "min(device.limits.maxComputeInvocationsPerWorkgroup, device.limits.maxComputeWorkgroupSizeX)",
107
+ "hiddenSize": "dim(shapes.input, ranks.input - 1)",
108
+ "numTokens": "numel(shapes.input) / max(1, hiddenSize)",
109
+ "topK": "attrs.k",
110
+ "weightBits": "attrs.expert_weight_bits",
111
+ "quantBlockSize": "attrs.block_size if has(attrs, \"block_size\") else 0",
112
+ "packSize": "2 if weightBits == 4 else 1",
113
+ "quantMidpoint": "8 if weightBits == 4 else 128",
114
+ "fusionSize": "2 if attrs.activation_type == \"swiglu\" and attrs.swiglu_fusion == 1 else 1",
115
+ "interSize": "dim(shapes.fc1_experts_weights, 1) / fusionSize",
116
+ "fc1PackedCols": "dim(shapes.fc1_experts_weights, 2)",
117
+ "fc2PackedCols": "dim(shapes.fc2_experts_weights, 2)",
118
+ "colWiseScales": "quantBlockSize == 0",
119
+ "fc1ScaleBlocks": "1 if colWiseScales else hiddenSize / max(1, quantBlockSize)",
120
+ "fc2ScaleBlocks": "1 if colWiseScales else interSize / max(1, quantBlockSize)",
121
+ "activationSupported": "(attrs.activation_type == \"relu\" and attrs.swiglu_fusion == 0) or (attrs.activation_type == \"swiglu\" and attrs.swiglu_fusion == 1)",
122
+ "routingModeSupported": "attrs.normalize_routing_weights == 0 or attrs.normalize_routing_weights == 1",
123
+ "rawWeightLayout": "attrs.weights_prepacked == -1 or attrs.weights_prepacked == 0",
124
+ "inputOutputShapeOk": "((ranks.input == 2 and ranks.output == 2 and dim(shapes.output, 0) == dim(shapes.input, 0) and dim(shapes.output, 1) == dim(shapes.input, 1)) or (ranks.input == 3 and ranks.output == 3 and dim(shapes.output, 0) == dim(shapes.input, 0) and dim(shapes.output, 1) == dim(shapes.input, 1) and dim(shapes.output, 2) == dim(shapes.input, 2))) and hiddenSize > 0",
125
+ "quantBlockSizeOk": "colWiseScales or (quantBlockSize >= 16 and pow2ceil(quantBlockSize) == quantBlockSize and hiddenSize % quantBlockSize == 0 and interSize % quantBlockSize == 0)",
126
+ "quantScalesOk": "tensorDtypes.fc1_scales == \"float32\" and tensorDtypes.fc2_scales == \"float32\" and dim(shapes.fc1_scales, 0) == dim(shapes.router_probs, 1) and dim(shapes.fc2_scales, 0) == dim(shapes.router_probs, 1) and dim(shapes.fc1_scales, 1) == dim(shapes.fc1_experts_weights, 1) and dim(shapes.fc2_scales, 1) == hiddenSize and ((ranks.fc1_scales == 2 and ranks.fc2_scales == 2) if colWiseScales else (ranks.fc1_scales == 3 and ranks.fc2_scales == 3 and dim(shapes.fc1_scales, 2) == fc1ScaleBlocks and dim(shapes.fc2_scales, 2) == fc2ScaleBlocks))",
127
+ "quantShapeOk": "inputOutputShapeOk and ranks.router_probs == 2 and ranks.fc1_experts_weights == 3 and ranks.fc2_experts_weights == 3 and dim(shapes.router_probs, 0) == numTokens and dim(shapes.fc1_experts_weights, 0) == dim(shapes.router_probs, 1) and dim(shapes.fc2_experts_weights, 0) == dim(shapes.router_probs, 1) and dim(shapes.fc1_experts_weights, 1) % fusionSize == 0 and dim(shapes.fc2_experts_weights, 1) == hiddenSize and dim(shapes.fc1_experts_weights, 2) * packSize == hiddenSize and dim(shapes.fc2_experts_weights, 2) * packSize == interSize and quantBlockSizeOk and quantScalesOk",
128
+ "quantContract": "activationSupported and routingModeSupported and rawWeightLayout and quantShapeOk and topK >= 1 and topK <= dim(shapes.router_probs, 1)",
129
+ "workgroupSizeOk": "tunables.workgroupSize >= 1 and tunables.workgroupSize <= deviceWorkgroupCap",
130
+ "hiddenChunkFits": "topK * interSize * 4 <= device.limits.maxStorageBufferBindingSize and topK * interSize * 4 <= device.limits.maxBufferSize",
131
+ "hiddenChunkTokens": "numTokens if interSize == 0 else min(numTokens, max(1, floor(min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize) / (topK * interSize * 4))))",
132
+ "hiddenChunkCount": "max(1, ceilDiv(numTokens, max(1, hiddenChunkTokens)))",
133
+ "routeScratchBytes": "numTokens * topK * 4",
134
+ "routedScratchFits": "routeScratchBytes <= device.limits.maxStorageBufferBindingSize and routeScratchBytes <= device.limits.maxBufferSize",
135
+ "groupTileM": "tunables.groupThreads * tunables.groupRegM",
136
+ "groupTileN": "tunables.groupThreads * tunables.groupRegN",
137
+ "groupTileKVec": "ceilDiv(tunables.groupTileK, 4)",
138
+ "groupThreadCount": "tunables.groupThreads * tunables.groupThreads",
139
+ "groupSlots": "hiddenChunkTokens * topK",
140
+ "groupMaxTiles": "ceilDiv(groupSlots, max(1, groupTileM)) + dim(shapes.router_probs, 1)",
141
+ "groupSharedBytes": "(groupTileM * tunables.groupTileK + 2 * groupTileN * tunables.groupTileK + groupTileM) * 4",
142
+ "groupSlotOutBytes": "hiddenChunkTokens * topK * hiddenSize * 4",
143
+ "groupedDeviceOk": "groupThreadCount <= device.limits.maxComputeInvocationsPerWorkgroup and tunables.groupThreads <= device.limits.maxComputeWorkgroupSizeX and tunables.groupThreads <= device.limits.maxComputeWorkgroupSizeY and tunables.groupRouteWorkgroup <= deviceWorkgroupCap and groupSharedBytes <= device.limits.maxComputeWorkgroupStorageSize and dim(shapes.router_probs, 1) * 8 <= device.limits.maxComputeWorkgroupStorageSize and tunables.groupTileK % 4 == 0",
144
+ "groupedDispatchOk": "groupMaxTiles <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(interSize, max(1, groupTileN)) <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(hiddenSize, max(1, groupTileN)) <= device.limits.maxComputeWorkgroupsPerDimension",
145
+ "groupSlotOutFits": "groupSlotOutBytes <= device.limits.maxStorageBufferBindingSize and groupSlotOutBytes <= device.limits.maxBufferSize",
146
+ "groupedShapeOk": "numTokens * topK * 4 >= groupTileM * dim(shapes.router_probs, 1)",
147
+ "groupedContract": "quantContract and workgroupSizeOk and hiddenChunkFits and routedScratchFits and interSize > 0 and groupedDeviceOk and groupedDispatchOk and groupSlotOutFits and groupedShapeOk",
148
+ "sgmatWorkgroup": "128",
149
+ "sgmatSubgroups": "4",
150
+ "sgmatRowSubtiles": "2",
151
+ "groupedSgmatSharedBytes": "groupTileM * 4 + groupTileM * 32 * 4 + 64 * 32 * 4 + sgmatSubgroups * 4 * 64 * 4",
152
+ "groupedSgmatOk": "groupedContract and groupTileM == 32 and interSize % 32 == 0 and groupedSgmatSharedBytes <= device.limits.maxComputeWorkgroupStorageSize and ceilDiv(hiddenSize, 64) <= device.limits.maxComputeWorkgroupsPerDimension and hiddenSize % 32 == 0",
153
+ "decodeLanesOk": "tunables.decodeLanes >= 1 and tunables.decodeLanes <= tunables.workgroupSize and tunables.workgroupSize % tunables.decodeLanes == 0",
154
+ "decodeRows": "max(1, tunables.workgroupSize / max(1, tunables.decodeLanes))",
155
+ "decodeDeviceOk": "workgroupSizeOk and decodeLanesOk and tunables.decodeLanes <= device.limits.maxComputeWorkgroupSizeX and decodeRows <= device.limits.maxComputeWorkgroupSizeY and tunables.workgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
156
+ "decodeDispatchOk": "ceilDiv(interSize, decodeRows) <= device.limits.maxComputeWorkgroupsPerDimension and ceilDiv(hiddenSize, decodeRows) <= device.limits.maxComputeWorkgroupsPerDimension and hiddenChunkTokens * topK <= device.limits.maxComputeWorkgroupsPerDimension",
157
+ "decodeLaneDepth": "min(hiddenSize, interSize) / max(1, tunables.decodeLanes)",
158
+ "decodeDepthOk": "decodeLaneDepth >= tunables.decodeMinLaneTrips",
159
+ "splitFc1Blocks": "ceilDiv(numTokens * topK * interSize, max(1, tunables.workgroupSize))",
160
+ "decodeOccupancyOk": "splitFc1Blocks <= tunables.decodeBlockTarget",
161
+ "decodeContract": "quantContract and decodeDeviceOk and decodeDispatchOk and hiddenChunkFits and routedScratchFits and interSize > 0 and decodeDepthOk and (decodeOccupancyOk or not groupedContract)"
162
+ },
163
+ "constants": {
164
+ "tokens": "numTokens",
165
+ "hidden": "hiddenSize",
166
+ "experts": "dim(shapes.router_probs, 1)",
167
+ "fc1Rows": "dim(shapes.fc1_experts_weights, 1)",
168
+ "inter": "interSize",
169
+ "topK": "topK",
170
+ "workgroupSize": "tunables.workgroupSize",
171
+ "activationType": "attrs.activation_type",
172
+ "hasSwigluLimit": "has(attrs, \"swiglu_limit\")",
173
+ "swigluLimit": "attrs.swiglu_limit if has(attrs, \"swiglu_limit\") else 0",
174
+ "quantMidpoint": "quantMidpoint",
175
+ "quantBlockSize": "quantBlockSize",
176
+ "packSize": "packSize",
177
+ "fc1PackedCols": "fc1PackedCols",
178
+ "fc2PackedCols": "fc2PackedCols",
179
+ "colWiseScales": "colWiseScales",
180
+ "fc1ScaleBlocks": "fc1ScaleBlocks",
181
+ "fc2ScaleBlocks": "fc2ScaleBlocks"
182
+ },
183
+ "bindingSets": {
184
+ "route": [
185
+ {
186
+ "name": "router_probs",
187
+ "arg": "routerT",
188
+ "semantic": "router_probs",
189
+ "buffer": { "type": "read-only-storage" },
190
+ "elementType": "f32"
191
+ },
192
+ { "name": "route_expert", "semantic": "routeExpert", "buffer": { "type": "storage" }, "elementType": "u32" },
193
+ { "name": "route_mix", "semantic": "routeMix", "buffer": { "type": "storage" }, "elementType": "f32" }
194
+ ],
195
+ "fc1Activation": [
196
+ {
197
+ "name": "input",
198
+ "arg": "inputT",
199
+ "semantic": "input",
200
+ "buffer": { "type": "read-only-storage" },
201
+ "elementType": "f32"
202
+ },
203
+ {
204
+ "name": "route_expert",
205
+ "semantic": "routeExpert",
206
+ "buffer": { "type": "read-only-storage" },
207
+ "elementType": "u32"
208
+ },
209
+ {
210
+ "name": "fc1_experts_weights",
211
+ "arg": "fc1T",
212
+ "semantic": "fc1_experts_weights",
213
+ "buffer": { "type": "read-only-storage" },
214
+ "elementType": "u32"
215
+ },
216
+ {
217
+ "name": "fc1_scales",
218
+ "arg": "fc1ScalesT",
219
+ "semantic": "fc1_scales",
220
+ "buffer": { "type": "read-only-storage" },
221
+ "elementType": "f32"
222
+ },
223
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "storage" }, "elementType": "f32" },
224
+ {
225
+ "name": "params",
226
+ "semantic": "kernel.params",
227
+ "buffer": { "type": "uniform" },
228
+ "struct": {
229
+ "name": "Params",
230
+ "fields": [
231
+ { "name": "activationAlpha", "type": "f32", "value": "attrs.activation_alpha" },
232
+ { "name": "activationBeta", "type": "f32", "value": "attrs.activation_beta" },
233
+ { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" },
234
+ {
235
+ "name": "tokenCount",
236
+ "type": "u32",
237
+ "value": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens)"
238
+ }
239
+ ]
240
+ }
241
+ }
242
+ ],
243
+ "output": [
244
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
245
+ {
246
+ "name": "route_expert",
247
+ "semantic": "routeExpert",
248
+ "buffer": { "type": "read-only-storage" },
249
+ "elementType": "u32"
250
+ },
251
+ { "name": "route_mix", "semantic": "routeMix", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
252
+ {
253
+ "name": "fc2_experts_weights",
254
+ "arg": "fc2T",
255
+ "semantic": "fc2_experts_weights",
256
+ "buffer": { "type": "read-only-storage" },
257
+ "elementType": "u32"
258
+ },
259
+ {
260
+ "name": "fc2_scales",
261
+ "arg": "fc2ScalesT",
262
+ "semantic": "fc2_scales",
263
+ "buffer": { "type": "read-only-storage" },
264
+ "elementType": "f32"
265
+ },
266
+ {
267
+ "name": "output",
268
+ "arg": "outputT",
269
+ "semantic": "output",
270
+ "buffer": { "type": "storage" },
271
+ "elementType": "f32"
272
+ },
273
+ {
274
+ "name": "params",
275
+ "semantic": "kernel.params",
276
+ "buffer": { "type": "uniform" },
277
+ "struct": {
278
+ "name": "Params",
279
+ "fields": [
280
+ { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" },
281
+ {
282
+ "name": "tokenCount",
283
+ "type": "u32",
284
+ "value": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens)"
285
+ }
286
+ ]
287
+ }
288
+ }
289
+ ],
290
+ "zeroInterOutput": [
291
+ {
292
+ "name": "output",
293
+ "arg": "outputT",
294
+ "semantic": "output",
295
+ "buffer": { "type": "storage" },
296
+ "elementType": "f32"
297
+ }
298
+ ],
299
+ "group": [
300
+ {
301
+ "name": "route_expert",
302
+ "semantic": "routeExpert",
303
+ "buffer": { "type": "read-only-storage" },
304
+ "elementType": "u32"
305
+ },
306
+ { "name": "slot_list", "semantic": "slotList", "buffer": { "type": "storage" }, "elementType": "u32" },
307
+ { "name": "tile_meta", "semantic": "tileMeta", "buffer": { "type": "storage" }, "elementType": "u32" },
308
+ {
309
+ "name": "params",
310
+ "semantic": "kernel.params",
311
+ "buffer": { "type": "uniform" },
312
+ "struct": {
313
+ "name": "Params",
314
+ "fields": [
315
+ { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" },
316
+ {
317
+ "name": "tokenCount",
318
+ "type": "u32",
319
+ "value": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens)"
320
+ }
321
+ ]
322
+ }
323
+ }
324
+ ],
325
+ "fc1Grouped": [
326
+ {
327
+ "name": "input",
328
+ "arg": "inputT",
329
+ "semantic": "input",
330
+ "buffer": { "type": "read-only-storage" },
331
+ "elementType": "f32"
332
+ },
333
+ { "name": "slot_list", "semantic": "slotList", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
334
+ { "name": "tile_meta", "semantic": "tileMeta", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
335
+ {
336
+ "name": "fc1_experts_weights",
337
+ "arg": "fc1T",
338
+ "semantic": "fc1_experts_weights",
339
+ "buffer": { "type": "read-only-storage" },
340
+ "elementType": "u32"
341
+ },
342
+ {
343
+ "name": "fc1_scales",
344
+ "arg": "fc1ScalesT",
345
+ "semantic": "fc1_scales",
346
+ "buffer": { "type": "read-only-storage" },
347
+ "elementType": "f32"
348
+ },
349
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "storage" }, "elementType": "f32" },
350
+ {
351
+ "name": "params",
352
+ "semantic": "kernel.params",
353
+ "buffer": { "type": "uniform" },
354
+ "struct": {
355
+ "name": "Params",
356
+ "fields": [
357
+ { "name": "activationAlpha", "type": "f32", "value": "attrs.activation_alpha" },
358
+ { "name": "activationBeta", "type": "f32", "value": "attrs.activation_beta" },
359
+ { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" }
360
+ ]
361
+ }
362
+ }
363
+ ],
364
+ "outputGrouped": [
365
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
366
+ { "name": "slot_list", "semantic": "slotList", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
367
+ { "name": "tile_meta", "semantic": "tileMeta", "buffer": { "type": "read-only-storage" }, "elementType": "u32" },
368
+ {
369
+ "name": "fc2_experts_weights",
370
+ "arg": "fc2T",
371
+ "semantic": "fc2_experts_weights",
372
+ "buffer": { "type": "read-only-storage" },
373
+ "elementType": "u32"
374
+ },
375
+ {
376
+ "name": "fc2_scales",
377
+ "arg": "fc2ScalesT",
378
+ "semantic": "fc2_scales",
379
+ "buffer": { "type": "read-only-storage" },
380
+ "elementType": "f32"
381
+ },
382
+ { "name": "slot_out", "semantic": "slotOut", "buffer": { "type": "storage" }, "elementType": "f32" }
383
+ ],
384
+ "mix": [
385
+ { "name": "slot_out", "semantic": "slotOut", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
386
+ { "name": "route_mix", "semantic": "routeMix", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
387
+ {
388
+ "name": "output",
389
+ "arg": "outputT",
390
+ "semantic": "output",
391
+ "buffer": { "type": "storage" },
392
+ "elementType": "f32"
393
+ },
394
+ {
395
+ "name": "params",
396
+ "semantic": "kernel.params",
397
+ "buffer": { "type": "uniform" },
398
+ "struct": {
399
+ "name": "Params",
400
+ "fields": [
401
+ { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" },
402
+ {
403
+ "name": "tokenCount",
404
+ "type": "u32",
405
+ "value": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens)"
406
+ }
407
+ ]
408
+ }
409
+ }
410
+ ],
411
+ "fc1ActivationIo": [
412
+ {
413
+ "name": "input",
414
+ "arg": "inputT",
415
+ "semantic": "input",
416
+ "buffer": { "type": "read-only-storage" },
417
+ "elementType": "f32"
418
+ },
419
+ {
420
+ "name": "route_expert",
421
+ "semantic": "routeExpert",
422
+ "buffer": { "type": "read-only-storage" },
423
+ "elementType": "u32"
424
+ },
425
+ {
426
+ "name": "fc1_experts_weights",
427
+ "arg": "fc1T",
428
+ "semantic": "fc1_experts_weights",
429
+ "buffer": { "type": "read-only-storage" },
430
+ "elementType": "u32"
431
+ },
432
+ {
433
+ "name": "fc1_scales",
434
+ "arg": "fc1ScalesT",
435
+ "semantic": "fc1_scales",
436
+ "buffer": { "type": "read-only-storage" },
437
+ "elementType": "f32"
438
+ },
439
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "storage" }, "elementType": "f32" }
440
+ ],
441
+ "fc1ActivationScheduled": [
442
+ {
443
+ "name": "input",
444
+ "arg": "inputT",
445
+ "semantic": "input",
446
+ "buffer": { "type": "read-only-storage" },
447
+ "elementType": "f32"
448
+ },
449
+ {
450
+ "name": "route_expert",
451
+ "semantic": "routeExpert",
452
+ "buffer": { "type": "read-only-storage" },
453
+ "elementType": "u32"
454
+ },
455
+ {
456
+ "name": "fc1_experts_weights",
457
+ "arg": "fc1T",
458
+ "semantic": "fc1_experts_weights",
459
+ "buffer": { "type": "read-only-storage" },
460
+ "elementType": "u32"
461
+ },
462
+ {
463
+ "name": "fc1_scales",
464
+ "arg": "fc1ScalesT",
465
+ "semantic": "fc1_scales",
466
+ "buffer": { "type": "read-only-storage" },
467
+ "elementType": "f32"
468
+ },
469
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "storage" }, "elementType": "f32" },
470
+ {
471
+ "name": "params",
472
+ "semantic": "kernel.params",
473
+ "buffer": { "type": "uniform" },
474
+ "struct": {
475
+ "name": "Params",
476
+ "fields": [
477
+ { "name": "activationAlpha", "type": "f32", "value": "attrs.activation_alpha" },
478
+ { "name": "activationBeta", "type": "f32", "value": "attrs.activation_beta" },
479
+ { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" }
480
+ ]
481
+ }
482
+ }
483
+ ],
484
+ "outputIo": [
485
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
486
+ {
487
+ "name": "route_expert",
488
+ "semantic": "routeExpert",
489
+ "buffer": { "type": "read-only-storage" },
490
+ "elementType": "u32"
491
+ },
492
+ { "name": "route_mix", "semantic": "routeMix", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
493
+ {
494
+ "name": "fc2_experts_weights",
495
+ "arg": "fc2T",
496
+ "semantic": "fc2_experts_weights",
497
+ "buffer": { "type": "read-only-storage" },
498
+ "elementType": "u32"
499
+ },
500
+ {
501
+ "name": "fc2_scales",
502
+ "arg": "fc2ScalesT",
503
+ "semantic": "fc2_scales",
504
+ "buffer": { "type": "read-only-storage" },
505
+ "elementType": "f32"
506
+ },
507
+ {
508
+ "name": "output",
509
+ "arg": "outputT",
510
+ "semantic": "output",
511
+ "buffer": { "type": "storage" },
512
+ "elementType": "f32"
513
+ }
514
+ ],
515
+ "outputScheduled": [
516
+ { "name": "hidden_act", "semantic": "hiddenAct", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
517
+ {
518
+ "name": "route_expert",
519
+ "semantic": "routeExpert",
520
+ "buffer": { "type": "read-only-storage" },
521
+ "elementType": "u32"
522
+ },
523
+ { "name": "route_mix", "semantic": "routeMix", "buffer": { "type": "read-only-storage" }, "elementType": "f32" },
524
+ {
525
+ "name": "fc2_experts_weights",
526
+ "arg": "fc2T",
527
+ "semantic": "fc2_experts_weights",
528
+ "buffer": { "type": "read-only-storage" },
529
+ "elementType": "u32"
530
+ },
531
+ {
532
+ "name": "fc2_scales",
533
+ "arg": "fc2ScalesT",
534
+ "semantic": "fc2_scales",
535
+ "buffer": { "type": "read-only-storage" },
536
+ "elementType": "f32"
537
+ },
538
+ {
539
+ "name": "output",
540
+ "arg": "outputT",
541
+ "semantic": "output",
542
+ "buffer": { "type": "storage" },
543
+ "elementType": "f32"
544
+ },
545
+ {
546
+ "name": "params",
547
+ "semantic": "kernel.params",
548
+ "buffer": { "type": "uniform" },
549
+ "struct": {
550
+ "name": "Params",
551
+ "fields": [{ "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" }]
552
+ }
553
+ }
554
+ ]
555
+ },
556
+ "variants": [
557
+ {
558
+ "id": "quant_zero_inter",
559
+ "priority": 20,
560
+ "when": ["quantContract", "workgroupSizeOk", "interSize == 0"],
561
+ "passes": [
562
+ {
563
+ "id": "output_stage",
564
+ "name": "QMoE.OutputStageZeroInter",
565
+ "source": {
566
+ "shader": "qmoe-output-zero-inter.wgsl.jinja",
567
+ "inputs": { "outputElementCount": "numel(shapes.output)" }
568
+ },
569
+ "bindings": "zeroInterOutput",
570
+ "dispatch": { "threads": "numel(shapes.output)", "workgroupSize": "constants.workgroupSize" }
571
+ }
572
+ ]
573
+ },
574
+ {
575
+ "id": "quant_grouped_sgmat_routed",
576
+ "priority": 32,
577
+ "when": ["groupedSgmatOk"],
578
+ "constants": {
579
+ "groupTileM": "groupTileM",
580
+ "groupRouteWorkgroup": "tunables.groupRouteWorkgroup",
581
+ "sgmatWorkgroup": "sgmatWorkgroup",
582
+ "sgmatSubgroups": "sgmatSubgroups",
583
+ "sgmatRowSubtiles": "sgmatRowSubtiles"
584
+ },
585
+ "intermediates": [
586
+ { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
587
+ { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
588
+ { "id": "hiddenAct", "dtype": "float32", "shape": "[hiddenChunkTokens * topK * interSize]" },
589
+ { "id": "slotList", "dtype": "uint32", "shape": "[max(1, groupSlots)]" },
590
+ { "id": "tileMeta", "dtype": "uint32", "shape": "[1 + 3 * groupMaxTiles]" },
591
+ { "id": "slotOut", "dtype": "float32", "shape": "[max(1, hiddenChunkTokens * topK * hiddenSize)]" }
592
+ ],
593
+ "passes": [
594
+ {
595
+ "id": "route_stage",
596
+ "name": "QMoE.RouteStage",
597
+ "shader": "qmoe-route-stage.wgsl.jinja",
598
+ "bindings": "route",
599
+ "dispatch": { "threads": "numTokens", "workgroupSize": "constants.workgroupSize" }
600
+ },
601
+ {
602
+ "id": "token_chunks",
603
+ "repeat": { "count": "hiddenChunkCount", "index": "chunk" },
604
+ "passes": [
605
+ {
606
+ "id": "group_stage",
607
+ "name": "QMoE.GroupStage",
608
+ "shader": "expert-group-slots.wgsl.jinja",
609
+ "bindings": "group",
610
+ "dispatch": {
611
+ "threads": "constants.groupRouteWorkgroup",
612
+ "workgroupSize": "constants.groupRouteWorkgroup"
613
+ }
614
+ },
615
+ {
616
+ "id": "fc1_activation_stage",
617
+ "name": "QMoE.FC1ActivationStageGroupedSgmat",
618
+ "shader": "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja",
619
+ "bindings": "fc1Grouped",
620
+ "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(interSize * fusionSize, 64)" }
621
+ },
622
+ {
623
+ "id": "output_stage",
624
+ "name": "QMoE.OutputStageGroupedSgmat",
625
+ "shader": "qmoe-output-grouped-sgmat.wgsl.jinja",
626
+ "bindings": "outputGrouped",
627
+ "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(hiddenSize, 64)" }
628
+ },
629
+ {
630
+ "id": "mix_stage",
631
+ "name": "QMoE.MixStage",
632
+ "shader": "qmoe-mix-stage.wgsl.jinja",
633
+ "bindings": "mix",
634
+ "dispatch": {
635
+ "threads": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize",
636
+ "workgroupSize": "constants.workgroupSize"
637
+ }
638
+ }
639
+ ]
640
+ }
641
+ ],
642
+ "requires": {
643
+ "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
644
+ "limits": { "maxComputeWorkgroupStorageSize": 16512 },
645
+ "subgroupMinSize": 32,
646
+ "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
647
+ }
648
+ },
649
+ {
650
+ "id": "quant_grouped_routed",
651
+ "priority": 30,
652
+ "when": ["groupedContract"],
653
+ "constants": {
654
+ "groupTileM": "groupTileM",
655
+ "groupTileN": "groupTileN",
656
+ "groupTileK": "tunables.groupTileK",
657
+ "groupTileKVec": "groupTileKVec",
658
+ "groupThreads": "tunables.groupThreads",
659
+ "regM": "tunables.groupRegM",
660
+ "regN": "tunables.groupRegN",
661
+ "groupRouteWorkgroup": "tunables.groupRouteWorkgroup"
662
+ },
663
+ "intermediates": [
664
+ { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
665
+ { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
666
+ { "id": "hiddenAct", "dtype": "float32", "shape": "[hiddenChunkTokens * topK * interSize]" },
667
+ { "id": "slotList", "dtype": "uint32", "shape": "[max(1, groupSlots)]" },
668
+ { "id": "tileMeta", "dtype": "uint32", "shape": "[1 + 3 * groupMaxTiles]" },
669
+ { "id": "slotOut", "dtype": "float32", "shape": "[max(1, hiddenChunkTokens * topK * hiddenSize)]" }
670
+ ],
671
+ "passes": [
672
+ {
673
+ "id": "route_stage",
674
+ "name": "QMoE.RouteStage",
675
+ "shader": "qmoe-route-stage.wgsl.jinja",
676
+ "bindings": "route",
677
+ "dispatch": { "threads": "numTokens", "workgroupSize": "constants.workgroupSize" }
678
+ },
679
+ {
680
+ "id": "token_chunks",
681
+ "repeat": { "count": "hiddenChunkCount", "index": "chunk" },
682
+ "passes": [
683
+ {
684
+ "id": "group_stage",
685
+ "name": "QMoE.GroupStage",
686
+ "shader": "expert-group-slots.wgsl.jinja",
687
+ "bindings": "group",
688
+ "dispatch": {
689
+ "threads": "constants.groupRouteWorkgroup",
690
+ "workgroupSize": "constants.groupRouteWorkgroup"
691
+ }
692
+ },
693
+ {
694
+ "id": "fc1_activation_stage",
695
+ "name": "QMoE.FC1ActivationStageGrouped",
696
+ "shader": "qmoe-fc1-activation-grouped.wgsl.jinja",
697
+ "bindings": "fc1Grouped",
698
+ "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(interSize, groupTileN)" }
699
+ },
700
+ {
701
+ "id": "output_stage",
702
+ "name": "QMoE.OutputStageGrouped",
703
+ "shader": "qmoe-output-grouped.wgsl.jinja",
704
+ "bindings": "outputGrouped",
705
+ "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(hiddenSize, groupTileN)" }
706
+ },
707
+ {
708
+ "id": "mix_stage",
709
+ "name": "QMoE.MixStage",
710
+ "shader": "qmoe-mix-stage.wgsl.jinja",
711
+ "bindings": "mix",
712
+ "dispatch": {
713
+ "threads": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize",
714
+ "workgroupSize": "constants.workgroupSize"
715
+ }
716
+ }
717
+ ]
718
+ }
719
+ ]
720
+ },
721
+ {
722
+ "id": "quant_gemv_routed",
723
+ "priority": 20,
724
+ "when": ["decodeContract"],
725
+ "constants": { "decodeLanes": "tunables.decodeLanes", "decodeRows": "decodeRows" },
726
+ "intermediates": [
727
+ { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
728
+ { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
729
+ { "id": "hiddenAct", "dtype": "float32", "shape": "[hiddenChunkTokens * topK * interSize]" }
730
+ ],
731
+ "passes": [
732
+ {
733
+ "id": "route_stage",
734
+ "name": "QMoE.RouteStage",
735
+ "shader": "qmoe-route-stage.wgsl.jinja",
736
+ "bindings": "route",
737
+ "dispatch": { "threads": "numTokens", "workgroupSize": "constants.workgroupSize" }
738
+ },
739
+ {
740
+ "id": "token_chunks",
741
+ "repeat": { "count": "hiddenChunkCount", "index": "chunk" },
742
+ "passes": [
743
+ {
744
+ "id": "fc1_activation_stage",
745
+ "name": "QMoE.FC1ActivationStageGemv",
746
+ "shader": "qmoe-fc1-activation-gemv.wgsl.jinja",
747
+ "bindings": "fc1ActivationScheduled",
748
+ "dispatch": {
749
+ "x": "ceilDiv(interSize, decodeRows)",
750
+ "y": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * topK"
751
+ }
752
+ },
753
+ {
754
+ "id": "output_stage",
755
+ "name": "QMoE.OutputStageGemv",
756
+ "shader": "qmoe-output-gemv.wgsl.jinja",
757
+ "bindings": "outputScheduled",
758
+ "dispatch": {
759
+ "x": "ceilDiv(hiddenSize, decodeRows)",
760
+ "y": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens)"
761
+ }
762
+ }
763
+ ]
764
+ }
765
+ ]
766
+ },
767
+ {
768
+ "id": "quant_split_routed",
769
+ "priority": 10,
770
+ "when": ["quantContract", "workgroupSizeOk", "hiddenChunkFits", "routedScratchFits", "interSize > 0"],
771
+ "intermediates": [
772
+ { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
773
+ { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
774
+ { "id": "hiddenAct", "dtype": "float32", "shape": "[hiddenChunkTokens * topK * interSize]" }
775
+ ],
776
+ "passes": [
777
+ {
778
+ "id": "route_stage",
779
+ "name": "QMoE.RouteStage",
780
+ "shader": "qmoe-route-stage.wgsl.jinja",
781
+ "bindings": "route",
782
+ "dispatch": { "threads": "numTokens", "workgroupSize": "constants.workgroupSize" }
783
+ },
784
+ {
785
+ "id": "token_chunks",
786
+ "repeat": { "count": "hiddenChunkCount", "index": "chunk" },
787
+ "passes": [
788
+ {
789
+ "id": "fc1_activation_stage",
790
+ "name": "QMoE.FC1ActivationStage",
791
+ "shader": "qmoe-fc1-activation-stage.wgsl.jinja",
792
+ "bindings": "fc1Activation",
793
+ "dispatch": {
794
+ "threads": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * topK * interSize",
795
+ "workgroupSize": "constants.workgroupSize"
796
+ }
797
+ },
798
+ {
799
+ "id": "output_stage",
800
+ "name": "QMoE.OutputStage",
801
+ "shader": "qmoe-output-stage.wgsl.jinja",
802
+ "bindings": "output",
803
+ "dispatch": {
804
+ "threads": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize",
805
+ "workgroupSize": "constants.workgroupSize"
806
+ }
807
+ }
808
+ ]
809
+ }
810
+ ]
811
+ }
812
+ ]
813
+ }
build/webgpu/metadata.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.QMoE",
3
+ "id": "_com_microsoft_qmoe_webgpu_9a12372",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "zBVupnayA0Ggc4y737P9tzqyM/q/N8F4j+FEJXz6Wks=",
11
+ "expert-group-slots.wgsl.jinja": "Ta+3H2FA1qRRzksdkwgKLZiO+AokmuM+B9JVN8cv1EQ=",
12
+ "manifest.json": "hZMDKX6e3562/hNqPc5QSpMZ7BRW0wcSrH63e8VTdM4=",
13
+ "qmoe-fc1-activation-gemv.wgsl.jinja": "UvP8Vyx1E9+/+I09w/z9iB6h6MHrSrOmjKfIelyIS0M=",
14
+ "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja": "bRKPszdbDTG0DlHeiiQ1lxyjJUJz0JVt9WZDsv8KVg8=",
15
+ "qmoe-fc1-activation-grouped.wgsl.jinja": "y2vGeNeXE5YmNrM2WrXogyF/fTvQ/nqYR4uWBTDvpPY=",
16
+ "qmoe-fc1-activation-stage.wgsl.jinja": "Hx7fYjr8CwB2jXeTnh6G6IyavWrLwyOdtUsJwVEpgGk=",
17
+ "qmoe-mix-stage.wgsl.jinja": "+deuFyuBn04UeIX8Hk2/ntCHNfM1vgkNDTcsh3uXVdI=",
18
+ "qmoe-output-gemv.wgsl.jinja": "OJLoDDWMYE5yaTeDGY7qUeuf6AepJL7ZZLx8ko6OTEQ=",
19
+ "qmoe-output-grouped-sgmat.wgsl.jinja": "XaFjSitK6tkK/QkrQ6wRConJLNekx+4u/FpxuFmh7DU=",
20
+ "qmoe-output-grouped.wgsl.jinja": "mHkrsubKwd0YWYo4TMpl4HWdbWheoeiAsUU2WsUON8E=",
21
+ "qmoe-output-stage.wgsl.jinja": "f9CWE9XR/5RTULNXdTHwA4gFsOL1QRg0H/rT3dD32Gs=",
22
+ "qmoe-output-zero-inter.wgsl.jinja": "6iBjk5YqxSSoXufDo/aJJRS4v8ZOp7wF2aRRXU4m1SA=",
23
+ "qmoe-route-stage.wgsl.jinja": "cGnJJB0bEdlcPFOKm8s42byI9ePixYfmfK9nK05L7V4=",
24
+ "test.json": "3i3FSregL3rNfwD/Wi7X2Ix296NNB1zIV+Ap9TrpMeU="
25
+ }
26
+ },
27
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
28
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.QMoE" }
29
+ }
build/webgpu/qmoe-fc1-activation-gemv.wgsl.jinja ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const HIDDEN: u32 = {{ hidden }}u;
4
+ const FC1_ROWS: u32 = {{ fc1Rows }}u;
5
+ const INTER: u32 = {{ inter }}u;
6
+ const TOP_K: u32 = {{ topK }}u;
7
+ // The split stage hands one thread a whole HIDDEN-long reduction, so a decode
8
+ // dispatch offers only tokens * TOP_K * INTER invocations. Here a workgroup owns
9
+ // ROWS intermediate columns of one routed slot and LANES lanes split the
10
+ // reduction, which is LANES times the invocations for the same arithmetic.
11
+ const LANES: u32 = {{ decodeLanes }}u;
12
+ const ROWS: u32 = {{ decodeRows }}u;
13
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
14
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
15
+ // first along the reduction axis, two codes per stored word.
16
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
17
+ const FC1_PACKED_COLS: u32 = {{ fc1PackedCols }}u;
18
+ {% if not colWiseScales %}
19
+ // Block-wise scales (fc1_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
20
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
21
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
22
+ const FC1_BLOCKS: u32 = {{ fc1ScaleBlocks }}u;
23
+ {% endif %}
24
+
25
+ fn fc1_weight(expert: u32, row: u32, col: u32) -> f32 {
26
+ let base = expert * FC1_ROWS + row;
27
+ {% if packSize == 2 %}
28
+ let word = fc1_experts_weights[base * FC1_PACKED_COLS + col / 2u];
29
+ let code = f32((word >> ((col & 1u) * 4u)) & 15u);
30
+ {% else %}
31
+ let code = f32(fc1_experts_weights[base * FC1_PACKED_COLS + col] & 255u);
32
+ {% endif %}
33
+ {% if not colWiseScales %}
34
+ return (code - MIDPOINT) * fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK];
35
+ {% else %}
36
+ return (code - MIDPOINT) * fc1_scales[base];
37
+ {% endif %}
38
+ }
39
+ // Decode two consecutive reduction-axis codes together. Four-bit codes share a
40
+ // stored word; eight-bit codes occupy adjacent words.
41
+ fn fc1_weight_pair(expert: u32, row: u32, col: u32) -> vec2<f32> {
42
+ let base = expert * FC1_ROWS + row;
43
+ {% if packSize == 2 %}
44
+ let word = fc1_experts_weights[base * FC1_PACKED_COLS + col / 2u];
45
+ let lo = f32(word & 15u) - MIDPOINT;
46
+ let hi = f32((word >> 4u) & 15u) - MIDPOINT;
47
+ {% else %}
48
+ let lo = f32(fc1_experts_weights[base * FC1_PACKED_COLS + col] & 255u) - MIDPOINT;
49
+ let hi = f32(fc1_experts_weights[base * FC1_PACKED_COLS + col + 1u] & 255u) - MIDPOINT;
50
+ {% endif %}
51
+ {% if not colWiseScales %}
52
+ return vec2<f32>(lo * fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK],
53
+ hi * fc1_scales[base * FC1_BLOCKS + (col + 1u) / QUANT_BLOCK]);
54
+ {% else %}
55
+ let scale = fc1_scales[base];
56
+ return vec2<f32>(lo * scale, hi * scale);
57
+ {% endif %}
58
+ }
59
+
60
+
61
+ {% if activationType == "swiglu" %}
62
+ fn activate(gate_in: f32, up_in: f32) -> f32 {
63
+ {% if hasSwigluLimit %}
64
+ // swiglu_limit clamps gate from above and up to [-limit, limit] before the
65
+ // product; when absent, no clamp is applied.
66
+ let gate = min(gate_in, {{ swigluLimit }});
67
+ let up = clamp(up_in, -({{ swigluLimit }}), {{ swigluLimit }});
68
+ {% else %}
69
+ let gate = gate_in;
70
+ let up = up_in;
71
+ {% endif %}
72
+ return gate / (1.0 + exp(-params.activationAlpha * gate)) * (up + params.activationBeta);
73
+ }
74
+ {% else %}
75
+ fn activate(value: f32) -> f32 {
76
+ return max(value, 0.0);
77
+ }
78
+ {% endif %}
79
+
80
+
81
+ {% macro rowlane_fold(arrays, lanes="LANES", lane="lane", slot="slot") %}
82
+ var n = {{ lanes }};
83
+ while (n > 1u) {
84
+ let half = (n + 1u) / 2u;
85
+ if ({{ lane }} + half < n) {
86
+ {% for a in arrays %}
87
+ {{ a }}[{{ slot }}] = {{ a }}[{{ slot }}] + {{ a }}[{{ slot }} + half];
88
+ {% endfor %}
89
+ }
90
+ workgroupBarrier();
91
+ n = half;
92
+ }
93
+ {%- endmacro %}
94
+
95
+ var<workgroup> gate_partial: array<f32, {{ decodeLanes * decodeRows }}>;
96
+ {% if activationType == "swiglu" %}
97
+ // The gate and up rows of an interleaved SwiGLU pair share the same activation
98
+ // value, so one workgroup reduces both and folds them together.
99
+ var<workgroup> up_partial: array<f32, {{ decodeLanes * decodeRows }}>;
100
+ {% endif %}
101
+
102
+ @compute @workgroup_size(LANES, ROWS, 1)
103
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
104
+ @builtin(local_invocation_id) lid: vec3<u32>) {
105
+ let i = wid.x * ROWS + lid.y;
106
+ let lane = lid.x;
107
+ // wid.y is the routed slot's index within this token chunk -- the split
108
+ // stage's `local_token * TOP_K + j` -- so hidden_act is addressed identically
109
+ // by both schedules.
110
+ let local_slot = wid.y;
111
+ let token = params.tokenOffset + local_slot / TOP_K;
112
+ let in_range = i < INTER;
113
+
114
+ var gate = 0.0;
115
+ {% if activationType == "swiglu" %}
116
+ var up = 0.0;
117
+ {% endif %}
118
+ if (in_range) {
119
+ let expert = route_expert[token * TOP_K + local_slot % TOP_K];
120
+ // Two reduction columns per step: at 4 bits they are one stored word.
121
+ for (var c = lane * 2u; c + 1u < HIDDEN; c = c + LANES * 2u) {
122
+ let x0 = f32(input[token * HIDDEN + c]);
123
+ let x1 = f32(input[token * HIDDEN + c + 1u]);
124
+ {% if activationType == "swiglu" %}
125
+ let g = fc1_weight_pair(expert, i * 2u, c);
126
+ let u = fc1_weight_pair(expert, i * 2u + 1u, c);
127
+ gate = gate + x0 * g.x + x1 * g.y;
128
+ up = up + x0 * u.x + x1 * u.y;
129
+ {% else %}
130
+ let g = fc1_weight_pair(expert, i, c);
131
+ gate = gate + x0 * g.x + x1 * g.y;
132
+ {% endif %}
133
+ }
134
+ // An odd reduction length leaves one column, which only 8-bit weights can have.
135
+ if ((HIDDEN & 1u) == 1u && lane == 0u) {
136
+ let h = HIDDEN - 1u;
137
+ let x = f32(input[token * HIDDEN + h]);
138
+ {% if activationType == "swiglu" %}
139
+ gate = gate + x * fc1_weight(expert, i * 2u, h);
140
+ up = up + x * fc1_weight(expert, i * 2u + 1u, h);
141
+ {% else %}
142
+ gate = gate + x * fc1_weight(expert, i, h);
143
+ {% endif %}
144
+ }
145
+ }
146
+
147
+ let slot = lid.y * LANES + lane;
148
+ gate_partial[slot] = gate;
149
+ {% if activationType == "swiglu" %}
150
+ up_partial[slot] = up;
151
+ {% endif %}
152
+ workgroupBarrier();
153
+ {% if activationType == "swiglu" %}
154
+ {{ rowlane_fold(["gate_partial", "up_partial"]) }}
155
+ {% else %}
156
+ {{ rowlane_fold(["gate_partial"]) }}
157
+ {% endif %}
158
+
159
+ if (lane == 0u && in_range) {
160
+ let base = lid.y * LANES;
161
+ {% if activationType == "swiglu" %}
162
+ hidden_act[local_slot * INTER + i] = activate(gate_partial[base], up_partial[base]);
163
+ {% else %}
164
+ hidden_act[local_slot * INTER + i] = activate(gate_partial[base]);
165
+ {% endif %}
166
+ }
167
+ }
build/webgpu/qmoe-fc1-activation-grouped-sgmat.wgsl.jinja ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ enable subgroups;
2
+ enable chromium_experimental_subgroup_matrix;
3
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
4
+
5
+ {{ env.wgsl.resourceDeclarations }}
6
+
7
+ // com.microsoft.QMoE, expert-grouped FC1 + activation stage on the subgroup-matrix tier.
8
+ //
9
+ // Same decomposition as the scalar grouped stage -- routed slots arrive sorted by expert,
10
+ // so a tile shares one expert and its weight tile is dequantized once and reused down the
11
+ // tile -- with the inner product on the matrix units and the packed weights dequantized
12
+ // straight into the shared B operand.
13
+ //
14
+ // A fused SwiGLU takes its gate from weight row 2c and its up from row 2c+1. Those are
15
+ // staged as adjacent columns of one B tile rather than as two tiles. The result columns a
16
+ // lane holds are already an even/odd pair, so the epilogue needs no reshuffle.
17
+ const HIDDEN: u32 = {{ hidden }}u;
18
+ const FC1_ROWS: u32 = {{ fc1Rows }}u;
19
+ const INTER: u32 = {{ inter }}u;
20
+ const TOP_K: u32 = {{ topK }}u;
21
+ const TILE_ROWS: u32 = {{ groupTileM }}u;
22
+ const TILE_COLS: u32 = 64u;
23
+ const TILE_K: u32 = 32u;
24
+ const SUB_COLS: u32 = 32u;
25
+ const SUB_ROWS: u32 = 16u;
26
+ {% set isSwiglu = activationType == "swiglu" %}
27
+ // Weight rows per output column: the fused gate/up pair, or one.
28
+ const FUSION: u32 = {{ 2 if isSwiglu else 1 }}u;
29
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
30
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
31
+ // first along the reduction axis, two codes per stored word.
32
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
33
+ const FC1_PACKED_COLS: u32 = {{ fc1PackedCols }}u;
34
+ {% if not colWiseScales %}
35
+ // Block-wise scales (fc1_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
36
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
37
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
38
+ const FC1_BLOCKS: u32 = {{ fc1ScaleBlocks }}u;
39
+ {% endif %}
40
+
41
+ // Decode the four-code group consumed by the staging loop. The aligned codes
42
+ // share one quantization scale: blocks are powers of two of at least 16, and
43
+ // the caller guarantees `col` is a multiple of four.
44
+ fn fc1_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
45
+ let base = expert * FC1_ROWS + row;
46
+ {% if packSize == 2 %}
47
+ let pair = base * FC1_PACKED_COLS + col / 2u;
48
+ let w0 = fc1_experts_weights[pair];
49
+ let w1 = fc1_experts_weights[pair + 1u];
50
+ let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
51
+ f32(w1 & 15u), f32((w1 >> 4u) & 15u));
52
+ {% else %}
53
+ let word = base * FC1_PACKED_COLS + col;
54
+ let codes = vec4<f32>(f32(fc1_experts_weights[word] & 255u), f32(fc1_experts_weights[word + 1u] & 255u),
55
+ f32(fc1_experts_weights[word + 2u] & 255u), f32(fc1_experts_weights[word + 3u] & 255u));
56
+ {% endif %}
57
+ {% if not colWiseScales %}
58
+ let scale = fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK];
59
+ {% else %}
60
+ let scale = fc1_scales[base];
61
+ {% endif %}
62
+ return (codes - vec4<f32>(MIDPOINT)) * scale;
63
+ }
64
+
65
+
66
+ {% if activationType == "swiglu" %}
67
+ fn activate(gate_in: f32, up_in: f32) -> f32 {
68
+ {% if hasSwigluLimit %}
69
+ // swiglu_limit clamps gate from above and up to [-limit, limit] before the
70
+ // product; when absent, no clamp is applied.
71
+ let gate = min(gate_in, {{ swigluLimit }});
72
+ let up = clamp(up_in, -({{ swigluLimit }}), {{ swigluLimit }});
73
+ {% else %}
74
+ let gate = gate_in;
75
+ let up = up_in;
76
+ {% endif %}
77
+ return gate / (1.0 + exp(-params.activationAlpha * gate)) * (up + params.activationBeta);
78
+ }
79
+ {% else %}
80
+ fn activate(value: f32) -> f32 {
81
+ return max(value, 0.0);
82
+ }
83
+ {% endif %}
84
+
85
+
86
+ var<workgroup> row_slot: array<u32, {{ groupTileM }}>;
87
+ var<workgroup> tile_A: array<f32, {{ groupTileM }} * 32>;
88
+ var<workgroup> tile_B: array<f32, 64 * 32>;
89
+ var<workgroup> scratch: array<array<array<f32, 64>, 4>, {{ sgmatSubgroups }}>;
90
+
91
+ // Rows past the tile's end stage zero and are dropped at the store, so a short final
92
+ // tile needs no separate arm.
93
+ fn loadSHMA(k_idx: u32, rows: u32, row: u32, c_idx: u32) {
94
+ let col = c_idx * 8u;
95
+ let base = (params.tokenOffset + row_slot[row] / TOP_K) * HIDDEN + k_idx + col;
96
+ if (row < rows) {
97
+ for (var i = 0u; i < 8u; i = i + 1u) {
98
+ tile_A[row * TILE_K + col + i] = f32(input[base + i]);
99
+ }
100
+ } else {
101
+ for (var i = 0u; i < 8u; i = i + 1u) {
102
+ tile_A[row * TILE_K + col + i] = 0.0;
103
+ }
104
+ }
105
+ }
106
+
107
+ // `row` indexes weight rows, so under SwiGLU it walks gate and up alternately. Rows whose
108
+ // output column is past INTER stage zero: they contribute nothing, are never stored, and
109
+ // their packed reads would be out of range.
110
+ fn loadSHMB(expert: u32, weight_base: u32, k_idx: u32, row: u32, c_idx: u32) {
111
+ let n = weight_base + row;
112
+ let col = c_idx * 16u;
113
+ let live = n / FUSION < INTER;
114
+ for (var i = 0u; i < 16u; i = i + 4u) {
115
+ var quad = vec4<f32>(0.0);
116
+ if (live) {
117
+ quad = fc1_weight_vec4(expert, n, k_idx + col + i);
118
+ }
119
+ let slot = row * TILE_K + col + i;
120
+ tile_B[slot] = quad.x;
121
+ tile_B[slot + 1u] = quad.y;
122
+ tile_B[slot + 2u] = quad.z;
123
+ tile_B[slot + 3u] = quad.w;
124
+ }
125
+ }
126
+
127
+ @compute @workgroup_size({{ sgmatWorkgroup }}, 1, 1)
128
+ fn main(
129
+ @builtin(workgroup_id) wid: vec3<u32>,
130
+ @builtin(local_invocation_index) local_idx: u32,
131
+ @builtin(subgroup_invocation_id) sg_id: u32,
132
+ @builtin(subgroup_size) sg_size: u32
133
+ ) {
134
+ let tile = wid.x;
135
+ if (tile >= tile_meta[0]) {
136
+ return;
137
+ }
138
+ let expert = tile_meta[1u + tile * 3u];
139
+ let slice_base = tile_meta[1u + tile * 3u + 1u];
140
+ let rows = tile_meta[1u + tile * 3u + 2u];
141
+ let weight_base = wid.y * TILE_COLS;
142
+
143
+ for (var m = local_idx; m < TILE_ROWS; m = m + {{ sgmatWorkgroup }}u) {
144
+ row_slot[m] = select(0u, slot_list[slice_base + m], m < rows);
145
+ }
146
+ workgroupBarrier();
147
+
148
+ let subtile_id = local_idx / sg_size;
149
+ let subtile_idx = subtile_id / {{ sgmatRowSubtiles }}u;
150
+ let subtile_idy = subtile_id % {{ sgmatRowSubtiles }}u;
151
+ let base_A = subtile_idy * SUB_ROWS;
152
+ let base_B = subtile_idx * SUB_COLS;
153
+
154
+ {% for row in [0, 1] %}{% for col in [0, 1, 2, 3] %}
155
+ var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, 8, 8>;
156
+ {% endfor %}{% endfor %}
157
+
158
+ for (var kidx = 0u; kidx < HIDDEN; kidx = kidx + TILE_K) {
159
+ loadSHMA(kidx, rows, local_idx / 4u, local_idx % 4u);
160
+ loadSHMB(expert, weight_base, kidx, local_idx / 2u, local_idx % 2u);
161
+ workgroupBarrier();
162
+
163
+ for (var step = 0u; step < TILE_K; step = step + 8u) {
164
+ {% set operandScalar = "f32" %} let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
165
+ var matA0: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>>(&tile_A, matrix_a_offset, false, TILE_K);
166
+ var matA1: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>>(&tile_A, matrix_a_offset + 8u * TILE_K, false, TILE_K);
167
+
168
+ let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
169
+ var matB0: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset, true, TILE_K);
170
+ var matB1: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 8u * TILE_K, true, TILE_K);
171
+ var matB2: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 16u * TILE_K, true, TILE_K);
172
+ var matB3: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 24u * TILE_K, true, TILE_K);
173
+
174
+ matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
175
+ matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
176
+ matC02 = subgroupMatrixMultiplyAccumulate(matA0, matB2, matC02);
177
+ matC03 = subgroupMatrixMultiplyAccumulate(matA0, matB3, matC03);
178
+ matC10 = subgroupMatrixMultiplyAccumulate(matA1, matB0, matC10);
179
+ matC11 = subgroupMatrixMultiplyAccumulate(matA1, matB1, matC11);
180
+ matC12 = subgroupMatrixMultiplyAccumulate(matA1, matB2, matC12);
181
+ matC13 = subgroupMatrixMultiplyAccumulate(matA1, matB3, matC13);
182
+ }
183
+ workgroupBarrier();
184
+ }
185
+
186
+ // A lane holds result columns `col` and `col2 = col + 1` of each 8-wide block, and `col`
187
+ // is even -- so under SwiGLU that pair is exactly one output column's gate and up.
188
+ let row = sg_id / 4u;
189
+ let col = (sg_id % 4u) * 2u;
190
+ let col2 = col + 1u;
191
+ let out_weight_base = weight_base + base_B;
192
+
193
+ {% for group in [0, 1] %}
194
+ // The scratch banks are shared by both row groups, so the readback is fenced on both
195
+ // sides of each collective store.
196
+ {% for c in [0, 1, 2, 3] %}
197
+ subgroupMatrixStore(&scratch[subtile_id][{{ c }}], 0u, matC{{ group }}{{ c }}, false, 8u);
198
+ {% endfor %}
199
+ workgroupBarrier();
200
+ {
201
+ let m = base_A + {{ 8 * group }}u + row;
202
+ if (m < rows) {
203
+ let out_base = row_slot[m] * INTER;
204
+ {% for c in [0, 1, 2, 3] %}
205
+ {% if isSwiglu %}
206
+ {
207
+ let out_col = (out_weight_base + col + {{ 8 * c }}u) / 2u;
208
+ if (out_col < INTER) {
209
+ hidden_act[out_base + out_col] = activate(scratch[subtile_id][{{ c }}][row * 8u + col],
210
+ scratch[subtile_id][{{ c }}][row * 8u + col2]);
211
+ }
212
+ }
213
+ {% else %}
214
+ {% for half in ['col', 'col2'] %}
215
+ {
216
+ let out_col = out_weight_base + {{ half }} + {{ 8 * c }}u;
217
+ if (out_col < INTER) {
218
+ hidden_act[out_base + out_col] = activate(scratch[subtile_id][{{ c }}][row * 8u + {{ half }}]);
219
+ }
220
+ }
221
+ {% endfor %}
222
+ {% endif %}
223
+ {% endfor %}
224
+ }
225
+ }
226
+ workgroupBarrier();
227
+ {% endfor %}
228
+ }
build/webgpu/qmoe-fc1-activation-grouped.wgsl.jinja ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // com.microsoft.QMoE, expert-grouped FC1 + activation stage.
4
+ // The group stage has sorted routed slots by expert, so an MTILE-row tile shares
5
+ // one expert. Its weight tile is dequantized once into workgroup memory and
6
+ // reused across every row.
7
+ const HIDDEN: u32 = {{ hidden }}u;
8
+ const FC1_ROWS: u32 = {{ fc1Rows }}u;
9
+ const INTER: u32 = {{ inter }}u;
10
+ const TOP_K: u32 = {{ topK }}u;
11
+ const MTILE: u32 = {{ groupTileM }}u;
12
+ const NTILE: u32 = {{ groupTileN }}u;
13
+ const KTILE: u32 = {{ groupTileK }}u;
14
+ const THREADS_SIDE: u32 = {{ groupThreads }}u;
15
+ const THREADS: u32 = THREADS_SIDE * THREADS_SIDE;
16
+ {% set isSwiglu = activationType == "swiglu" %}
17
+ {% set primaryRow = "col * 2u" if isSwiglu else "col" %}
18
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
19
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
20
+ // first along the reduction axis, two codes per stored word.
21
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
22
+ const FC1_PACKED_COLS: u32 = {{ fc1PackedCols }}u;
23
+ {% if not colWiseScales %}
24
+ // Block-wise scales (fc1_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
25
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
26
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
27
+ const FC1_BLOCKS: u32 = {{ fc1ScaleBlocks }}u;
28
+ {% endif %}
29
+
30
+ fn fc1_weight(expert: u32, row: u32, col: u32) -> f32 {
31
+ let base = expert * FC1_ROWS + row;
32
+ {% if packSize == 2 %}
33
+ let word = fc1_experts_weights[base * FC1_PACKED_COLS + col / 2u];
34
+ let code = f32((word >> ((col & 1u) * 4u)) & 15u);
35
+ {% else %}
36
+ let code = f32(fc1_experts_weights[base * FC1_PACKED_COLS + col] & 255u);
37
+ {% endif %}
38
+ {% if not colWiseScales %}
39
+ return (code - MIDPOINT) * fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK];
40
+ {% else %}
41
+ return (code - MIDPOINT) * fc1_scales[base];
42
+ {% endif %}
43
+ }
44
+ // Decode the four-code group consumed by the staging loop. The aligned codes
45
+ // share one quantization scale: blocks are powers of two of at least 16, and
46
+ // the caller guarantees `col` is a multiple of four.
47
+ fn fc1_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
48
+ let base = expert * FC1_ROWS + row;
49
+ {% if packSize == 2 %}
50
+ let pair = base * FC1_PACKED_COLS + col / 2u;
51
+ let w0 = fc1_experts_weights[pair];
52
+ let w1 = fc1_experts_weights[pair + 1u];
53
+ let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
54
+ f32(w1 & 15u), f32((w1 >> 4u) & 15u));
55
+ {% else %}
56
+ let word = base * FC1_PACKED_COLS + col;
57
+ let codes = vec4<f32>(f32(fc1_experts_weights[word] & 255u), f32(fc1_experts_weights[word + 1u] & 255u),
58
+ f32(fc1_experts_weights[word + 2u] & 255u), f32(fc1_experts_weights[word + 3u] & 255u));
59
+ {% endif %}
60
+ {% if not colWiseScales %}
61
+ let scale = fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK];
62
+ {% else %}
63
+ let scale = fc1_scales[base];
64
+ {% endif %}
65
+ return (codes - vec4<f32>(MIDPOINT)) * scale;
66
+ }
67
+
68
+
69
+ {% if activationType == "swiglu" %}
70
+ fn activate(gate_in: f32, up_in: f32) -> f32 {
71
+ {% if hasSwigluLimit %}
72
+ // swiglu_limit clamps gate from above and up to [-limit, limit] before the
73
+ // product; when absent, no clamp is applied.
74
+ let gate = min(gate_in, {{ swigluLimit }});
75
+ let up = clamp(up_in, -({{ swigluLimit }}), {{ swigluLimit }});
76
+ {% else %}
77
+ let gate = gate_in;
78
+ let up = up_in;
79
+ {% endif %}
80
+ return gate / (1.0 + exp(-params.activationAlpha * gate)) * (up + params.activationBeta);
81
+ }
82
+ {% else %}
83
+ fn activate(value: f32) -> f32 {
84
+ return max(value, 0.0);
85
+ }
86
+ {% endif %}
87
+
88
+
89
+ const KVEC: u32 = {{ groupTileKVec }}u;
90
+ {% macro stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, guarded, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
91
+ for (var idx = tid; idx < MTILE * KVEC; idx = idx + THREADS) {
92
+ let m = idx / KVEC;
93
+ let kb = (idx % KVEC) * 4u;
94
+ var a_vec: vec4<f32>;
95
+ {% if aLoadVec4 and not guarded %}
96
+ a_vec = {{ aLoadVec4 }};
97
+ {% else %}
98
+ {% for j in range(4) %}
99
+ {
100
+ let k = kb + {{ j }}u;
101
+ a_vec[{{ j }}] = {{ ("select(0.0, " ~ aLoad ~ ", k_base + k < " ~ kExtent ~ ")") if guarded else aLoad }};
102
+ }
103
+ {% endfor %}
104
+ {% endif %}
105
+ a_tile[idx] = a_vec;
106
+ }
107
+ for (var idx = tid; idx < NTILE * KVEC; idx = idx + THREADS) {
108
+ let n = idx / KVEC;
109
+ let kb = (idx % KVEC) * 4u;
110
+ let col = n_base + n;
111
+ {% if guarded %}
112
+ let col_live = col < {{ nExtent }};
113
+ {% endif %}
114
+ var b_vec: vec4<f32>;
115
+ {% if b2Load %}
116
+ var b2_vec: vec4<f32>;
117
+ {% endif %}
118
+ {% if bLoadVec4 and not guarded %}
119
+ b_vec = {{ bLoadVec4 }};
120
+ {% if b2Load %}
121
+ b2_vec = {{ b2LoadVec4 }};
122
+ {% endif %}
123
+ {% else %}
124
+ {% for j in range(4) %}
125
+ {
126
+ let k = kb + {{ j }}u;
127
+ {% if guarded %}
128
+ let live = col_live && k_base + k < {{ kExtent }};
129
+ {% endif %}
130
+ b_vec[{{ j }}] = {{ ("select(0.0, " ~ bLoad ~ ", live)") if guarded else bLoad }};
131
+ {% if b2Load %}
132
+ b2_vec[{{ j }}] = {{ ("select(0.0, " ~ b2Load ~ ", live)") if guarded else b2Load }};
133
+ {% endif %}
134
+ }
135
+ {% endfor %}
136
+ {% endif %}
137
+ b_tile[idx] = b_vec;
138
+ {% if b2Load %}
139
+ b2_tile[idx] = b2_vec;
140
+ {% endif %}
141
+ }
142
+ {%- endmacro %}
143
+
144
+ {% macro group_tile_loop(aLoad, bLoad, b2Load, kExtent, nExtent, regM, regN, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
145
+ {% for r in range(regM) %}
146
+ {% for c in range(regN) %}
147
+ var acc_{{ r }}_{{ c }} = 0.0;
148
+ {% if b2Load %}
149
+ var acc2_{{ r }}_{{ c }} = 0.0;
150
+ {% endif %}
151
+ {% endfor %}
152
+ {% endfor %}
153
+
154
+ // A tile that lies wholly inside both extents needs no per-element bounds check, and
155
+ // that is every tile but the edges: the guards below exist for a reduction axis or an
156
+ // output width the tile size does not divide, yet the interior tiles were paying four
157
+ // comparisons and four selects per staged vec4 -- against 64 fused multiply-adds of
158
+ // actual work. Both arms stage identical values. The test is workgroup-uniform, so the
159
+ // barrier after it is still reached by every invocation together.
160
+ let n_full = n_base + NTILE <= {{ nExtent }};
161
+ var k_base = 0u;
162
+ loop {
163
+ if (k_base >= {{ kExtent }}) {
164
+ break;
165
+ }
166
+ if (n_full && k_base + KTILE <= {{ kExtent }}) {
167
+ {{ stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, false, bLoadVec4, b2LoadVec4, aLoadVec4) }}
168
+ } else {
169
+ {{ stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, true) }}
170
+ }
171
+ workgroupBarrier();
172
+
173
+ for (var kv = 0u; kv < KVEC; kv = kv + 1u) {
174
+ {% for r in range(regM) %}
175
+ let a{{ r }} = a_tile[(lid.y * {{ regM }}u + {{ r }}u) * KVEC + kv];
176
+ {% endfor %}
177
+ {% for c in range(regN) %}
178
+ let b{{ c }} = b_tile[(lid.x * {{ regN }}u + {{ c }}u) * KVEC + kv];
179
+ {% if b2Load %}
180
+ let s{{ c }} = b2_tile[(lid.x * {{ regN }}u + {{ c }}u) * KVEC + kv];
181
+ {% endif %}
182
+ {% endfor %}
183
+ {% for r in range(regM) %}
184
+ {% for c in range(regN) %}
185
+ acc_{{ r }}_{{ c }} = acc_{{ r }}_{{ c }} + dot(a{{ r }}, b{{ c }});
186
+ {% if b2Load %}
187
+ acc2_{{ r }}_{{ c }} = acc2_{{ r }}_{{ c }} + dot(a{{ r }}, s{{ c }});
188
+ {% endif %}
189
+ {% endfor %}
190
+ {% endfor %}
191
+ }
192
+ // Orders this step's tile reads before the next step overwrites them.
193
+ workgroupBarrier();
194
+ k_base = k_base + KTILE;
195
+ }
196
+ {%- endmacro %}
197
+
198
+ var<workgroup> row_slot: array<u32, {{ groupTileM }}>;
199
+ var<workgroup> a_tile: array<vec4<f32>, {{ groupTileM * groupTileKVec }}>;
200
+ var<workgroup> b_tile: array<vec4<f32>, {{ groupTileN * groupTileKVec }}>;
201
+ {% if isSwiglu %}
202
+ var<workgroup> b2_tile: array<vec4<f32>, {{ groupTileN * groupTileKVec }}>;
203
+ {% endif %}
204
+
205
+ @compute @workgroup_size(THREADS_SIDE, THREADS_SIDE, 1)
206
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
207
+ @builtin(local_invocation_id) lid: vec3<u32>) {
208
+ let tile = wid.x;
209
+ if (tile >= tile_meta[0]) {
210
+ return;
211
+ }
212
+ let expert = tile_meta[1u + tile * 3u];
213
+ let slice_base = tile_meta[1u + tile * 3u + 1u];
214
+ let rows = tile_meta[1u + tile * 3u + 2u];
215
+ let n_base = wid.y * NTILE;
216
+ let tid = lid.y * THREADS_SIDE + lid.x;
217
+
218
+ for (var m = tid; m < MTILE; m = m + THREADS) {
219
+ // Rows past this tile's end park on row 0; their results are discarded at the store.
220
+ row_slot[m] = select(0u, slot_list[slice_base + m], m < rows);
221
+ }
222
+ workgroupBarrier();
223
+ {{ group_tile_loop("f32(input[(params.tokenOffset + row_slot[m] / TOP_K) * HIDDEN + k_base + k])",
224
+ "fc1_weight(expert, " ~ primaryRow ~ ", k_base + k)",
225
+ "fc1_weight(expert, col * 2u + 1u, k_base + k)" if isSwiglu else "",
226
+ "HIDDEN", "INTER", regM, regN,
227
+ "fc1_weight_vec4(expert, " ~ primaryRow ~ ", k_base + kb)",
228
+ "fc1_weight_vec4(expert, col * 2u + 1u, k_base + kb)" if isSwiglu else "") }}
229
+
230
+ {% for r in range(regM) %}
231
+ {% for c in range(regN) %}
232
+ {
233
+ let m = lid.y * {{ regM }}u + {{ r }}u;
234
+ let col = n_base + lid.x * {{ regN }}u + {{ c }}u;
235
+ if (m < rows && col < INTER) {
236
+ {% if isSwiglu %}
237
+ hidden_act[row_slot[m] * INTER + col] = activate(acc_{{ r }}_{{ c }}, acc2_{{ r }}_{{ c }});
238
+ {% else %}
239
+ hidden_act[row_slot[m] * INTER + col] = activate(acc_{{ r }}_{{ c }});
240
+ {% endif %}
241
+ }
242
+ }
243
+ {% endfor %}
244
+ {% endfor %}
245
+ }
build/webgpu/qmoe-fc1-activation-stage.wgsl.jinja ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const HIDDEN: u32 = {{ hidden }}u;
4
+ const FC1_ROWS: u32 = {{ fc1Rows }}u;
5
+ const INTER: u32 = {{ inter }}u;
6
+ // WGSL rejects a const-expression `/ 0u` outright; guard the divisor. INTER == 0 makes `total` 0,
7
+ // so the index bounds-guard returns every thread before this divisor is used (degenerate empty
8
+ // inter-dim case).
9
+ const INTER_DIV: u32 = max(1u, INTER);
10
+ const WG: u32 = {{ workgroupSize }}u;
11
+ {% if topK > 1 %}
12
+ const TOP_K: u32 = {{ topK }}u;
13
+ {% endif %}
14
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
15
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
16
+ // first along the reduction axis, two codes per stored word.
17
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
18
+ const FC1_PACKED_COLS: u32 = {{ fc1PackedCols }}u;
19
+ {% if not colWiseScales %}
20
+ // Block-wise scales (fc1_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
21
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
22
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
23
+ const FC1_BLOCKS: u32 = {{ fc1ScaleBlocks }}u;
24
+ {% endif %}
25
+
26
+ fn fc1_weight(expert: u32, row: u32, col: u32) -> f32 {
27
+ let base = expert * FC1_ROWS + row;
28
+ {% if packSize == 2 %}
29
+ let word = fc1_experts_weights[base * FC1_PACKED_COLS + col / 2u];
30
+ let code = f32((word >> ((col & 1u) * 4u)) & 15u);
31
+ {% else %}
32
+ let code = f32(fc1_experts_weights[base * FC1_PACKED_COLS + col] & 255u);
33
+ {% endif %}
34
+ {% if not colWiseScales %}
35
+ return (code - MIDPOINT) * fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK];
36
+ {% else %}
37
+ return (code - MIDPOINT) * fc1_scales[base];
38
+ {% endif %}
39
+ }
40
+
41
+
42
+ {% if activationType == "swiglu" %}
43
+ fn activate(gate_in: f32, up_in: f32) -> f32 {
44
+ {% if hasSwigluLimit %}
45
+ // swiglu_limit clamps gate from above and up to [-limit, limit] before the
46
+ // product; when absent, no clamp is applied.
47
+ let gate = min(gate_in, {{ swigluLimit }});
48
+ let up = clamp(up_in, -({{ swigluLimit }}), {{ swigluLimit }});
49
+ {% else %}
50
+ let gate = gate_in;
51
+ let up = up_in;
52
+ {% endif %}
53
+ return gate / (1.0 + exp(-params.activationAlpha * gate)) * (up + params.activationBeta);
54
+ }
55
+ {% else %}
56
+ fn activate(value: f32) -> f32 {
57
+ return max(value, 0.0);
58
+ }
59
+ {% endif %}
60
+
61
+
62
+ @compute @workgroup_size(WG, 1, 1)
63
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
64
+ @builtin(num_workgroups) nwg: vec3<u32>) {
65
+ // 2D-folded flat index: gid.y carries the high bits past the maxComputeWorkgroupsPerDimension
66
+ // workgroup-per-dimension dispatch limit. Reduces to gid.x when nwg.y == 1.
67
+ let index = gid.x + gid.y * nwg.x * WG;
68
+ {% if topK > 1 %}
69
+ // One activation row per routed slot rather than per token: the grid is
70
+ // (token, slot, inter) and hidden_act is addressed by the same flat index.
71
+ let total = params.tokenCount * TOP_K * INTER;
72
+ if (index >= total) {
73
+ return;
74
+ }
75
+
76
+ let slot = index / INTER_DIV;
77
+ let local_token = slot / TOP_K;
78
+ let token = params.tokenOffset + local_token;
79
+ let i = index % INTER_DIV;
80
+
81
+ let expert = route_expert[token * TOP_K + slot % TOP_K];
82
+ {% else %}
83
+ let total = params.tokenCount * INTER;
84
+ if (index >= total) {
85
+ return;
86
+ }
87
+
88
+ let local_token = index / INTER_DIV;
89
+ let token = params.tokenOffset + local_token;
90
+ let i = index % INTER_DIV;
91
+
92
+ let expert = route_expert[token];
93
+ {% endif %}
94
+
95
+ var gate = 0.0;
96
+ {% if activationType == "swiglu" %}
97
+ var up = 0.0;
98
+ {% endif %}
99
+ for (var h: u32 = 0u; h < HIDDEN; h = h + 1u) {
100
+ let x = f32(input[token * HIDDEN + h]);
101
+ {% if activationType == "swiglu" %}
102
+ gate = gate + x * fc1_weight(expert, i * 2u, h);
103
+ {% else %}
104
+ gate = gate + x * fc1_weight(expert, i, h);
105
+ {% endif %}
106
+ {% if activationType == "swiglu" %}
107
+ up = up + x * fc1_weight(expert, i * 2u + 1u, h);
108
+ {% endif %}
109
+ }
110
+
111
+ {% if activationType == "swiglu" %}
112
+ hidden_act[index] = activate(gate, up);
113
+ {% else %}
114
+ hidden_act[index] = activate(gate);
115
+ {% endif %}
116
+ }
build/webgpu/qmoe-mix-stage.wgsl.jinja ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // Routed sum for the grouped schedule. The grouped FC2 stage produces one projected row per
4
+ // routed SLOT because a token's slots are spread across different expert tiles; this pass
5
+ // applies each slot's routing weight and collapses the TOP_K slots into the token's output row.
6
+ // The expert's FC2 bias is already inside its slot's row, so it is scaled by that slot's weight
7
+ // exactly as the other schedules scale it.
8
+ const HIDDEN: u32 = {{ hidden }}u;
9
+ const TOP_K: u32 = {{ topK }}u;
10
+ const WG: u32 = {{ workgroupSize }}u;
11
+
12
+ @compute @workgroup_size(WG, 1, 1)
13
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
14
+ @builtin(num_workgroups) nwg: vec3<u32>) {
15
+ // 2D-folded flat index: gid.y carries the high bits past the maxComputeWorkgroupsPerDimension
16
+ // workgroup-per-dimension dispatch limit. Reduces to gid.x when nwg.y == 1.
17
+ let index = gid.x + gid.y * nwg.x * WG;
18
+ let total = params.tokenCount * HIDDEN;
19
+ if (index >= total) {
20
+ return;
21
+ }
22
+
23
+ let local_token = index / HIDDEN;
24
+ let token = params.tokenOffset + local_token;
25
+ let out_col = index % HIDDEN;
26
+
27
+ var acc = 0.0;
28
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
29
+ acc = acc + slot_out[(local_token * TOP_K + j) * HIDDEN + out_col] * route_mix[token * TOP_K + j];
30
+ }
31
+
32
+ output[token * HIDDEN + out_col] = acc;
33
+ }
build/webgpu/qmoe-output-gemv.wgsl.jinja ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const HIDDEN: u32 = {{ hidden }}u;
4
+ const INTER: u32 = {{ inter }}u;
5
+ const TOP_K: u32 = {{ topK }}u;
6
+ // A workgroup owns ROWS output columns of one token and LANES lanes split the
7
+ // intermediate axis, so the split stage's per-thread INTER-long walk becomes a
8
+ // LANES-wide cooperative reduction.
9
+ const LANES: u32 = {{ decodeLanes }}u;
10
+ const ROWS: u32 = {{ decodeRows }}u;
11
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
12
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
13
+ // first along the reduction axis, two codes per stored word.
14
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
15
+ const FC2_PACKED_COLS: u32 = {{ fc2PackedCols }}u;
16
+ {% if not colWiseScales %}
17
+ // Block-wise scales (fc2_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
18
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
19
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
20
+ const FC2_BLOCKS: u32 = {{ fc2ScaleBlocks }}u;
21
+ {% endif %}
22
+
23
+ fn fc2_weight(expert: u32, row: u32, col: u32) -> f32 {
24
+ let base = expert * HIDDEN + row;
25
+ {% if packSize == 2 %}
26
+ let word = fc2_experts_weights[base * FC2_PACKED_COLS + col / 2u];
27
+ let code = f32((word >> ((col & 1u) * 4u)) & 15u);
28
+ {% else %}
29
+ let code = f32(fc2_experts_weights[base * FC2_PACKED_COLS + col] & 255u);
30
+ {% endif %}
31
+ {% if not colWiseScales %}
32
+ return (code - MIDPOINT) * fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK];
33
+ {% else %}
34
+ return (code - MIDPOINT) * fc2_scales[base];
35
+ {% endif %}
36
+ }
37
+ // Decode two consecutive reduction-axis codes together. Four-bit codes share a
38
+ // stored word; eight-bit codes occupy adjacent words.
39
+ fn fc2_weight_pair(expert: u32, row: u32, col: u32) -> vec2<f32> {
40
+ let base = expert * HIDDEN + row;
41
+ {% if packSize == 2 %}
42
+ let word = fc2_experts_weights[base * FC2_PACKED_COLS + col / 2u];
43
+ let lo = f32(word & 15u) - MIDPOINT;
44
+ let hi = f32((word >> 4u) & 15u) - MIDPOINT;
45
+ {% else %}
46
+ let lo = f32(fc2_experts_weights[base * FC2_PACKED_COLS + col] & 255u) - MIDPOINT;
47
+ let hi = f32(fc2_experts_weights[base * FC2_PACKED_COLS + col + 1u] & 255u) - MIDPOINT;
48
+ {% endif %}
49
+ {% if not colWiseScales %}
50
+ return vec2<f32>(lo * fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK],
51
+ hi * fc2_scales[base * FC2_BLOCKS + (col + 1u) / QUANT_BLOCK]);
52
+ {% else %}
53
+ let scale = fc2_scales[base];
54
+ return vec2<f32>(lo * scale, hi * scale);
55
+ {% endif %}
56
+ }
57
+
58
+
59
+ {% macro rowlane_fold(arrays, lanes="LANES", lane="lane", slot="slot") %}
60
+ var n = {{ lanes }};
61
+ while (n > 1u) {
62
+ let half = (n + 1u) / 2u;
63
+ if ({{ lane }} + half < n) {
64
+ {% for a in arrays %}
65
+ {{ a }}[{{ slot }}] = {{ a }}[{{ slot }}] + {{ a }}[{{ slot }} + half];
66
+ {% endfor %}
67
+ }
68
+ workgroupBarrier();
69
+ n = half;
70
+ }
71
+ {%- endmacro %}
72
+
73
+ var<workgroup> partial: array<f32, {{ decodeLanes * decodeRows }}>;
74
+
75
+ @compute @workgroup_size(LANES, ROWS, 1)
76
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
77
+ @builtin(local_invocation_id) lid: vec3<u32>) {
78
+ let out_col = wid.x * ROWS + lid.y;
79
+ let lane = lid.x;
80
+ let local_token = wid.y;
81
+ let token = params.tokenOffset + local_token;
82
+ let in_range = out_col < HIDDEN;
83
+
84
+ var acc = 0.0;
85
+ if (in_range) {
86
+ // route_mix is uniform across a workgroup's lanes, so folding each slot's
87
+ // share into its own partial here is the same value as scaling the reduced
88
+ // sum -- and it leaves ONE cross-lane fold covering all TOP_K slots.
89
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
90
+ let route_slot = token * TOP_K + j;
91
+ let expert = route_expert[route_slot];
92
+ let act_base = (local_token * TOP_K + j) * INTER;
93
+ var part = 0.0;
94
+ // Two reduction columns per step: at 4 bits they are one stored word.
95
+ for (var c = lane * 2u; c + 1u < INTER; c = c + LANES * 2u) {
96
+ let p = fc2_weight_pair(expert, out_col, c);
97
+ part = part + hidden_act[act_base + c] * p.x
98
+ + hidden_act[act_base + c + 1u] * p.y;
99
+ }
100
+ // An odd reduction length leaves one column, which only 8-bit weights can have.
101
+ if ((INTER & 1u) == 1u && lane == 0u) {
102
+ let i = INTER - 1u;
103
+ part = part + hidden_act[act_base + i] * fc2_weight(expert, out_col, i);
104
+ }
105
+ acc = acc + part * route_mix[route_slot];
106
+ }
107
+ }
108
+
109
+ let slot = lid.y * LANES + lane;
110
+ partial[slot] = acc;
111
+ workgroupBarrier();
112
+ {{ rowlane_fold(["partial"]) }}
113
+
114
+ if (lane == 0u && in_range) {
115
+ output[token * HIDDEN + out_col] = partial[lid.y * LANES];
116
+ }
117
+ }
build/webgpu/qmoe-output-grouped-sgmat.wgsl.jinja ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ enable subgroups;
2
+ enable chromium_experimental_subgroup_matrix;
3
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
4
+
5
+ {{ env.wgsl.resourceDeclarations }}
6
+
7
+ // com.microsoft.QMoE, expert-grouped FC2 stage on the subgroup-matrix tier.
8
+ //
9
+ // Same decomposition as the scalar grouped stage -- the group stage has sorted routed
10
+ // slots by expert, so one tile shares an expert and its weight tile is dequantized once
11
+ // and reused across the tile's rows -- but the inner product runs on the matrix units.
12
+ // The packed weights are dequantized straight into the shared B tile, so no float weight
13
+ // matrix is ever materialized, and the accumulate stays f32, which is exact for a 4-bit
14
+ // code times its scale.
15
+ //
16
+ // The M axis is the tile's routed-slot slice rather than a contiguous row range, so the
17
+ // A loader gathers through `slot_list` and the store scatters back through it.
18
+ const HIDDEN: u32 = {{ hidden }}u;
19
+ const INTER: u32 = {{ inter }}u;
20
+ const TILE_ROWS: u32 = {{ groupTileM }}u;
21
+ const TILE_COLS: u32 = 64u;
22
+ const TILE_K: u32 = 32u;
23
+ const SUB_COLS: u32 = 32u;
24
+ const SUB_ROWS: u32 = 16u;
25
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
26
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
27
+ // first along the reduction axis, two codes per stored word.
28
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
29
+ const FC2_PACKED_COLS: u32 = {{ fc2PackedCols }}u;
30
+ {% if not colWiseScales %}
31
+ // Block-wise scales (fc2_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
32
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
33
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
34
+ const FC2_BLOCKS: u32 = {{ fc2ScaleBlocks }}u;
35
+ {% endif %}
36
+
37
+ // Decode the four-code group consumed by the staging loop. The aligned codes
38
+ // share one quantization scale: blocks are powers of two of at least 16, and
39
+ // the caller guarantees `col` is a multiple of four.
40
+ fn fc2_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
41
+ let base = expert * HIDDEN + row;
42
+ {% if packSize == 2 %}
43
+ let pair = base * FC2_PACKED_COLS + col / 2u;
44
+ let w0 = fc2_experts_weights[pair];
45
+ let w1 = fc2_experts_weights[pair + 1u];
46
+ let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
47
+ f32(w1 & 15u), f32((w1 >> 4u) & 15u));
48
+ {% else %}
49
+ let word = base * FC2_PACKED_COLS + col;
50
+ let codes = vec4<f32>(f32(fc2_experts_weights[word] & 255u), f32(fc2_experts_weights[word + 1u] & 255u),
51
+ f32(fc2_experts_weights[word + 2u] & 255u), f32(fc2_experts_weights[word + 3u] & 255u));
52
+ {% endif %}
53
+ {% if not colWiseScales %}
54
+ let scale = fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK];
55
+ {% else %}
56
+ let scale = fc2_scales[base];
57
+ {% endif %}
58
+ return (codes - vec4<f32>(MIDPOINT)) * scale;
59
+ }
60
+
61
+
62
+ var<workgroup> row_slot: array<u32, {{ groupTileM }}>;
63
+ var<workgroup> tile_A: array<f32, {{ groupTileM }} * 32>;
64
+ var<workgroup> tile_B: array<f32, 64 * 32>;
65
+ var<workgroup> scratch: array<array<array<f32, 64>, 4>, {{ sgmatSubgroups }}>;
66
+
67
+ // Rows past the tile's end stage zero and are dropped at the store, so a short final
68
+ // tile needs no separate arm.
69
+ fn loadSHMA(k_idx: u32, rows: u32, row: u32, c_idx: u32) {
70
+ let col = c_idx * 8u;
71
+ let base = row_slot[row] * INTER + k_idx + col;
72
+ if (row < rows) {
73
+ for (var i = 0u; i < 8u; i = i + 1u) {
74
+ tile_A[row * TILE_K + col + i] = hidden_act[base + i];
75
+ }
76
+ } else {
77
+ for (var i = 0u; i < 8u; i = i + 1u) {
78
+ tile_A[row * TILE_K + col + i] = 0.0;
79
+ }
80
+ }
81
+ }
82
+
83
+ // Columns past HIDDEN stage zero: they contribute nothing to the product and are never
84
+ // stored, and their packed reads would be out of range.
85
+ fn loadSHMB(expert: u32, col_base: u32, k_idx: u32, row: u32, c_idx: u32) {
86
+ let n = col_base + row;
87
+ let col = c_idx * 16u;
88
+ for (var i = 0u; i < 16u; i = i + 4u) {
89
+ let k = k_idx + col + i;
90
+ var quad = vec4<f32>(0.0);
91
+ if (n < HIDDEN) {
92
+ quad = fc2_weight_vec4(expert, n, k);
93
+ }
94
+ tile_B[row * TILE_K + col + i] = quad.x;
95
+ tile_B[row * TILE_K + col + i + 1u] = quad.y;
96
+ tile_B[row * TILE_K + col + i + 2u] = quad.z;
97
+ tile_B[row * TILE_K + col + i + 3u] = quad.w;
98
+ }
99
+ }
100
+
101
+ // One output column of an 8-wide result block, scattered back to its routed slot.
102
+ fn storeCol(col_base: u32, row: u32, cc: u32, slot: u32, src_slot: u32, out_slot: u32) {
103
+ let c = cc + slot * 8u;
104
+ if (col_base + c < HIDDEN) {
105
+ slot_out[out_slot * HIDDEN + col_base + c] = scratch[src_slot][slot][row * 8u + cc];
106
+ }
107
+ }
108
+
109
+ fn storeOutput(col_base: u32, row: u32, col: u32, src_slot: u32, row_base: u32, rows: u32) {
110
+ let m = row_base + row;
111
+ if (m < rows) {
112
+ let col2 = col + 1u;
113
+ let out_slot = row_slot[m];
114
+ {% for blockCol in [0, 1, 2, 3] %}
115
+ storeCol(col_base, row, col, {{ blockCol }}u, src_slot, out_slot);
116
+ storeCol(col_base, row, col2, {{ blockCol }}u, src_slot, out_slot);
117
+ {% endfor %}
118
+ }
119
+ }
120
+
121
+ @compute @workgroup_size({{ sgmatWorkgroup }}, 1, 1)
122
+ fn main(
123
+ @builtin(workgroup_id) wid: vec3<u32>,
124
+ @builtin(local_invocation_index) local_idx: u32,
125
+ @builtin(subgroup_invocation_id) sg_id: u32,
126
+ @builtin(subgroup_size) sg_size: u32
127
+ ) {
128
+ let tile = wid.x;
129
+ if (tile >= tile_meta[0]) {
130
+ return;
131
+ }
132
+ let expert = tile_meta[1u + tile * 3u];
133
+ let slice_base = tile_meta[1u + tile * 3u + 1u];
134
+ let rows = tile_meta[1u + tile * 3u + 2u];
135
+ let col_base = wid.y * TILE_COLS;
136
+
137
+ for (var m = local_idx; m < TILE_ROWS; m = m + {{ sgmatWorkgroup }}u) {
138
+ row_slot[m] = select(0u, slot_list[slice_base + m], m < rows);
139
+ }
140
+ workgroupBarrier();
141
+
142
+ let subtile_id = local_idx / sg_size;
143
+ let subtile_idx = subtile_id / {{ sgmatRowSubtiles }}u;
144
+ let subtile_idy = subtile_id % {{ sgmatRowSubtiles }}u;
145
+ let base_A = subtile_idy * SUB_ROWS;
146
+ let base_B = subtile_idx * SUB_COLS;
147
+
148
+ {% for row in [0, 1] %}{% for col in [0, 1, 2, 3] %}
149
+ var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, 8, 8>;
150
+ {% endfor %}{% endfor %}
151
+
152
+ for (var kidx = 0u; kidx < INTER; kidx = kidx + TILE_K) {
153
+ loadSHMA(kidx, rows, local_idx / 4u, local_idx % 4u);
154
+ loadSHMB(expert, col_base, kidx, local_idx / 2u, local_idx % 2u);
155
+ workgroupBarrier();
156
+
157
+ for (var step = 0u; step < TILE_K; step = step + 8u) {
158
+ {% set operandScalar = "f32" %} let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
159
+ var matA0: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>>(&tile_A, matrix_a_offset, false, TILE_K);
160
+ var matA1: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>>(&tile_A, matrix_a_offset + 8u * TILE_K, false, TILE_K);
161
+
162
+ let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
163
+ var matB0: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset, true, TILE_K);
164
+ var matB1: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 8u * TILE_K, true, TILE_K);
165
+ var matB2: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 16u * TILE_K, true, TILE_K);
166
+ var matB3: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 24u * TILE_K, true, TILE_K);
167
+
168
+ matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
169
+ matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
170
+ matC02 = subgroupMatrixMultiplyAccumulate(matA0, matB2, matC02);
171
+ matC03 = subgroupMatrixMultiplyAccumulate(matA0, matB3, matC03);
172
+ matC10 = subgroupMatrixMultiplyAccumulate(matA1, matB0, matC10);
173
+ matC11 = subgroupMatrixMultiplyAccumulate(matA1, matB1, matC11);
174
+ matC12 = subgroupMatrixMultiplyAccumulate(matA1, matB2, matC12);
175
+ matC13 = subgroupMatrixMultiplyAccumulate(matA1, matB3, matC13);
176
+ }
177
+ workgroupBarrier();
178
+ }
179
+
180
+ // Each scratch bank is written by a collective store and then read across lanes, and
181
+ // the two row groups reuse the same banks, so both barriers are load-bearing.
182
+ {% for col in [0, 1, 2, 3] %}
183
+ subgroupMatrixStore(&scratch[subtile_id][{{ col }}], 0u, matC0{{ col }}, false, 8u);
184
+ {% endfor %}
185
+ workgroupBarrier();
186
+ let row = sg_id / 4u;
187
+ let col = (sg_id % 4u) * 2u;
188
+ storeOutput(col_base + base_B, row, col, subtile_id, base_A, rows);
189
+ workgroupBarrier();
190
+
191
+ {% for col in [0, 1, 2, 3] %}
192
+ subgroupMatrixStore(&scratch[subtile_id][{{ col }}], 0u, matC1{{ col }}, false, 8u);
193
+ {% endfor %}
194
+ workgroupBarrier();
195
+ storeOutput(col_base + base_B, row, col, subtile_id, base_A + 8u, rows);
196
+ }
build/webgpu/qmoe-output-grouped.wgsl.jinja ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ // com.microsoft.QMoE, expert-grouped FC2 stage, sharing the tile loop with the grouped FC1
4
+ // stage: the reduction is the intermediate axis and the output width is the hidden axis.
5
+ //
6
+ // The result is written per slot rather than per token because routed slots land in
7
+ // different expert tiles and nothing here can sum them. The mix stage that follows applies each
8
+ // slot's routing weight and collapses the TOP_K slots into the token's output row.
9
+ const HIDDEN: u32 = {{ hidden }}u;
10
+ const INTER: u32 = {{ inter }}u;
11
+ const MTILE: u32 = {{ groupTileM }}u;
12
+ const NTILE: u32 = {{ groupTileN }}u;
13
+ const KTILE: u32 = {{ groupTileK }}u;
14
+ const THREADS_SIDE: u32 = {{ groupThreads }}u;
15
+ const THREADS: u32 = THREADS_SIDE * THREADS_SIDE;
16
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
17
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
18
+ // first along the reduction axis, two codes per stored word.
19
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
20
+ const FC2_PACKED_COLS: u32 = {{ fc2PackedCols }}u;
21
+ {% if not colWiseScales %}
22
+ // Block-wise scales (fc2_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
23
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
24
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
25
+ const FC2_BLOCKS: u32 = {{ fc2ScaleBlocks }}u;
26
+ {% endif %}
27
+
28
+ fn fc2_weight(expert: u32, row: u32, col: u32) -> f32 {
29
+ let base = expert * HIDDEN + row;
30
+ {% if packSize == 2 %}
31
+ let word = fc2_experts_weights[base * FC2_PACKED_COLS + col / 2u];
32
+ let code = f32((word >> ((col & 1u) * 4u)) & 15u);
33
+ {% else %}
34
+ let code = f32(fc2_experts_weights[base * FC2_PACKED_COLS + col] & 255u);
35
+ {% endif %}
36
+ {% if not colWiseScales %}
37
+ return (code - MIDPOINT) * fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK];
38
+ {% else %}
39
+ return (code - MIDPOINT) * fc2_scales[base];
40
+ {% endif %}
41
+ }
42
+ // Decode the four-code group consumed by the staging loop. The aligned codes
43
+ // share one quantization scale: blocks are powers of two of at least 16, and
44
+ // the caller guarantees `col` is a multiple of four.
45
+ fn fc2_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
46
+ let base = expert * HIDDEN + row;
47
+ {% if packSize == 2 %}
48
+ let pair = base * FC2_PACKED_COLS + col / 2u;
49
+ let w0 = fc2_experts_weights[pair];
50
+ let w1 = fc2_experts_weights[pair + 1u];
51
+ let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
52
+ f32(w1 & 15u), f32((w1 >> 4u) & 15u));
53
+ {% else %}
54
+ let word = base * FC2_PACKED_COLS + col;
55
+ let codes = vec4<f32>(f32(fc2_experts_weights[word] & 255u), f32(fc2_experts_weights[word + 1u] & 255u),
56
+ f32(fc2_experts_weights[word + 2u] & 255u), f32(fc2_experts_weights[word + 3u] & 255u));
57
+ {% endif %}
58
+ {% if not colWiseScales %}
59
+ let scale = fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK];
60
+ {% else %}
61
+ let scale = fc2_scales[base];
62
+ {% endif %}
63
+ return (codes - vec4<f32>(MIDPOINT)) * scale;
64
+ }
65
+
66
+
67
+ const KVEC: u32 = {{ groupTileKVec }}u;
68
+ {% macro stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, guarded, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
69
+ for (var idx = tid; idx < MTILE * KVEC; idx = idx + THREADS) {
70
+ let m = idx / KVEC;
71
+ let kb = (idx % KVEC) * 4u;
72
+ var a_vec: vec4<f32>;
73
+ {% if aLoadVec4 and not guarded %}
74
+ a_vec = {{ aLoadVec4 }};
75
+ {% else %}
76
+ {% for j in range(4) %}
77
+ {
78
+ let k = kb + {{ j }}u;
79
+ a_vec[{{ j }}] = {{ ("select(0.0, " ~ aLoad ~ ", k_base + k < " ~ kExtent ~ ")") if guarded else aLoad }};
80
+ }
81
+ {% endfor %}
82
+ {% endif %}
83
+ a_tile[idx] = a_vec;
84
+ }
85
+ for (var idx = tid; idx < NTILE * KVEC; idx = idx + THREADS) {
86
+ let n = idx / KVEC;
87
+ let kb = (idx % KVEC) * 4u;
88
+ let col = n_base + n;
89
+ {% if guarded %}
90
+ let col_live = col < {{ nExtent }};
91
+ {% endif %}
92
+ var b_vec: vec4<f32>;
93
+ {% if b2Load %}
94
+ var b2_vec: vec4<f32>;
95
+ {% endif %}
96
+ {% if bLoadVec4 and not guarded %}
97
+ b_vec = {{ bLoadVec4 }};
98
+ {% if b2Load %}
99
+ b2_vec = {{ b2LoadVec4 }};
100
+ {% endif %}
101
+ {% else %}
102
+ {% for j in range(4) %}
103
+ {
104
+ let k = kb + {{ j }}u;
105
+ {% if guarded %}
106
+ let live = col_live && k_base + k < {{ kExtent }};
107
+ {% endif %}
108
+ b_vec[{{ j }}] = {{ ("select(0.0, " ~ bLoad ~ ", live)") if guarded else bLoad }};
109
+ {% if b2Load %}
110
+ b2_vec[{{ j }}] = {{ ("select(0.0, " ~ b2Load ~ ", live)") if guarded else b2Load }};
111
+ {% endif %}
112
+ }
113
+ {% endfor %}
114
+ {% endif %}
115
+ b_tile[idx] = b_vec;
116
+ {% if b2Load %}
117
+ b2_tile[idx] = b2_vec;
118
+ {% endif %}
119
+ }
120
+ {%- endmacro %}
121
+
122
+ {% macro group_tile_loop(aLoad, bLoad, b2Load, kExtent, nExtent, regM, regN, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
123
+ {% for r in range(regM) %}
124
+ {% for c in range(regN) %}
125
+ var acc_{{ r }}_{{ c }} = 0.0;
126
+ {% if b2Load %}
127
+ var acc2_{{ r }}_{{ c }} = 0.0;
128
+ {% endif %}
129
+ {% endfor %}
130
+ {% endfor %}
131
+
132
+ // A tile that lies wholly inside both extents needs no per-element bounds check, and
133
+ // that is every tile but the edges: the guards below exist for a reduction axis or an
134
+ // output width the tile size does not divide, yet the interior tiles were paying four
135
+ // comparisons and four selects per staged vec4 -- against 64 fused multiply-adds of
136
+ // actual work. Both arms stage identical values. The test is workgroup-uniform, so the
137
+ // barrier after it is still reached by every invocation together.
138
+ let n_full = n_base + NTILE <= {{ nExtent }};
139
+ var k_base = 0u;
140
+ loop {
141
+ if (k_base >= {{ kExtent }}) {
142
+ break;
143
+ }
144
+ if (n_full && k_base + KTILE <= {{ kExtent }}) {
145
+ {{ stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, false, bLoadVec4, b2LoadVec4, aLoadVec4) }}
146
+ } else {
147
+ {{ stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, true) }}
148
+ }
149
+ workgroupBarrier();
150
+
151
+ for (var kv = 0u; kv < KVEC; kv = kv + 1u) {
152
+ {% for r in range(regM) %}
153
+ let a{{ r }} = a_tile[(lid.y * {{ regM }}u + {{ r }}u) * KVEC + kv];
154
+ {% endfor %}
155
+ {% for c in range(regN) %}
156
+ let b{{ c }} = b_tile[(lid.x * {{ regN }}u + {{ c }}u) * KVEC + kv];
157
+ {% if b2Load %}
158
+ let s{{ c }} = b2_tile[(lid.x * {{ regN }}u + {{ c }}u) * KVEC + kv];
159
+ {% endif %}
160
+ {% endfor %}
161
+ {% for r in range(regM) %}
162
+ {% for c in range(regN) %}
163
+ acc_{{ r }}_{{ c }} = acc_{{ r }}_{{ c }} + dot(a{{ r }}, b{{ c }});
164
+ {% if b2Load %}
165
+ acc2_{{ r }}_{{ c }} = acc2_{{ r }}_{{ c }} + dot(a{{ r }}, s{{ c }});
166
+ {% endif %}
167
+ {% endfor %}
168
+ {% endfor %}
169
+ }
170
+ // Orders this step's tile reads before the next step overwrites them.
171
+ workgroupBarrier();
172
+ k_base = k_base + KTILE;
173
+ }
174
+ {%- endmacro %}
175
+
176
+ var<workgroup> row_slot: array<u32, {{ groupTileM }}>;
177
+ var<workgroup> a_tile: array<vec4<f32>, {{ groupTileM * groupTileKVec }}>;
178
+ var<workgroup> b_tile: array<vec4<f32>, {{ groupTileN * groupTileKVec }}>;
179
+
180
+ @compute @workgroup_size(THREADS_SIDE, THREADS_SIDE, 1)
181
+ fn main(@builtin(workgroup_id) wid: vec3<u32>,
182
+ @builtin(local_invocation_id) lid: vec3<u32>) {
183
+ let tile = wid.x;
184
+ if (tile >= tile_meta[0]) {
185
+ return;
186
+ }
187
+ let expert = tile_meta[1u + tile * 3u];
188
+ let slice_base = tile_meta[1u + tile * 3u + 1u];
189
+ let rows = tile_meta[1u + tile * 3u + 2u];
190
+ let n_base = wid.y * NTILE;
191
+ let tid = lid.y * THREADS_SIDE + lid.x;
192
+
193
+ for (var m = tid; m < MTILE; m = m + THREADS) {
194
+ // Rows past this tile's end park on row 0; their results are discarded at the store.
195
+ row_slot[m] = select(0u, slot_list[slice_base + m], m < rows);
196
+ }
197
+ workgroupBarrier();
198
+ {{ group_tile_loop("hidden_act[row_slot[m] * INTER + k_base + k]",
199
+ "fc2_weight(expert, col, k_base + k)",
200
+ "", "INTER", "HIDDEN", regM, regN,
201
+ "fc2_weight_vec4(expert, col, k_base + kb)") }}
202
+
203
+ {% for r in range(regM) %}
204
+ {% for c in range(regN) %}
205
+ {
206
+ let m = lid.y * {{ regM }}u + {{ r }}u;
207
+ let col = n_base + lid.x * {{ regN }}u + {{ c }}u;
208
+ if (m < rows && col < HIDDEN) {
209
+ slot_out[row_slot[m] * HIDDEN + col] = acc_{{ r }}_{{ c }};
210
+ }
211
+ }
212
+ {% endfor %}
213
+ {% endfor %}
214
+ }
build/webgpu/qmoe-output-stage.wgsl.jinja ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const HIDDEN: u32 = {{ hidden }}u;
4
+ const INTER: u32 = {{ inter }}u;
5
+ const WG: u32 = {{ workgroupSize }}u;
6
+ {% if topK > 1 %}
7
+ const TOP_K: u32 = {{ topK }}u;
8
+ {% endif %}
9
+ // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
10
+ // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
11
+ // first along the reduction axis, two codes per stored word.
12
+ const MIDPOINT: f32 = f32({{ quantMidpoint }});
13
+ const FC2_PACKED_COLS: u32 = {{ fc2PackedCols }}u;
14
+ {% if not colWiseScales %}
15
+ // Block-wise scales (fc2_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
16
+ // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
17
+ const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
18
+ const FC2_BLOCKS: u32 = {{ fc2ScaleBlocks }}u;
19
+ {% endif %}
20
+
21
+ fn fc2_weight(expert: u32, row: u32, col: u32) -> f32 {
22
+ let base = expert * HIDDEN + row;
23
+ {% if packSize == 2 %}
24
+ let word = fc2_experts_weights[base * FC2_PACKED_COLS + col / 2u];
25
+ let code = f32((word >> ((col & 1u) * 4u)) & 15u);
26
+ {% else %}
27
+ let code = f32(fc2_experts_weights[base * FC2_PACKED_COLS + col] & 255u);
28
+ {% endif %}
29
+ {% if not colWiseScales %}
30
+ return (code - MIDPOINT) * fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK];
31
+ {% else %}
32
+ return (code - MIDPOINT) * fc2_scales[base];
33
+ {% endif %}
34
+ }
35
+
36
+
37
+ @compute @workgroup_size(WG, 1, 1)
38
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
39
+ @builtin(num_workgroups) nwg: vec3<u32>) {
40
+ // 2D-folded flat index: gid.y carries the high bits past the maxComputeWorkgroupsPerDimension
41
+ // workgroup-per-dimension dispatch limit. Reduces to gid.x when nwg.y == 1.
42
+ let index = gid.x + gid.y * nwg.x * WG;
43
+ let total = params.tokenCount * HIDDEN;
44
+ if (index >= total) {
45
+ return;
46
+ }
47
+
48
+ let local_token = index / HIDDEN;
49
+ let token = params.tokenOffset + local_token;
50
+ let out_col = index % HIDDEN;
51
+
52
+ {% if topK > 1 %}
53
+ // Each routed slot contributes its own fc2 projection of its own activation row, weighted by
54
+ // that slot's share. Summing per slot (rather than mixing the activations first) keeps the
55
+ // K == 1 term exactly the `acc * mix` the top-1 stage computes.
56
+ var acc = 0.0;
57
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
58
+ let expert = route_expert[token * TOP_K + j];
59
+ var part = 0.0;
60
+ for (var i: u32 = 0u; i < INTER; i = i + 1u) {
61
+ part = part + hidden_act[(local_token * TOP_K + j) * INTER + i] * fc2_weight(expert, out_col, i);
62
+ }
63
+ acc = acc + part * route_mix[token * TOP_K + j];
64
+ }
65
+
66
+ output[token * HIDDEN + out_col] = acc;
67
+ {% else %}
68
+ let expert = route_expert[token];
69
+ let mix = route_mix[token];
70
+
71
+ var acc = 0.0;
72
+ for (var i: u32 = 0u; i < INTER; i = i + 1u) {
73
+ acc = acc + hidden_act[local_token * INTER + i] * fc2_weight(expert, out_col, i);
74
+ }
75
+
76
+ output[token * HIDDEN + out_col] = acc * mix;
77
+ {% endif %}
78
+ }
build/webgpu/qmoe-output-zero-inter.wgsl.jinja ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const WG: u32 = {{ workgroupSize }}u;
4
+
5
+ @compute @workgroup_size(WG, 1, 1)
6
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
7
+ @builtin(num_workgroups) nwg: vec3<u32>) {
8
+ let index = gid.x + gid.y * nwg.x * WG;
9
+ if (index < {{ source.outputElementCount }}u) {
10
+ output[index] = 0.0;
11
+ }
12
+ }
build/webgpu/qmoe-route-stage.wgsl.jinja ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {{ env.wgsl.resourceDeclarations }}
2
+
3
+ const TOKENS: u32 = {{ tokens }}u;
4
+ const EXPERTS: u32 = {{ experts }}u;
5
+ const WG: u32 = {{ workgroupSize }}u;
6
+ {% if topK > 1 %}
7
+ const TOP_K: u32 = {{ topK }}u;
8
+ // Serial top-K selection in O(TOP_K * EXPERTS), followed by a stable softmax
9
+ // over the selected logits. Equal logits select the higher expert index.
10
+ struct TopKRoute {
11
+ experts: array<u32, TOP_K>,
12
+ weights: array<f32, TOP_K>,
13
+ }
14
+
15
+ fn topk_route(token: u32) -> TopKRoute {
16
+ var result: TopKRoute;
17
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
18
+ // EXPERTS is the "nothing picked yet" sentinel; the contract keeps TOP_K <= EXPERTS, so
19
+ // every pass finds an untaken expert and no slot can keep it.
20
+ var best_expert = EXPERTS;
21
+ var best_weight = 0.0;
22
+ for (var e = 0u; e < EXPERTS; e = e + 1u) {
23
+ var taken = false;
24
+ for (var p = 0u; p < j; p = p + 1u) {
25
+ if (result.experts[p] == e) {
26
+ taken = true;
27
+ }
28
+ }
29
+ if (!taken) {
30
+ let candidate = router_probs[token * EXPERTS + e];
31
+ if (best_expert == EXPERTS || candidate > best_weight ||
32
+ (candidate == best_weight && e > best_expert)) {
33
+ best_expert = e;
34
+ best_weight = candidate;
35
+ }
36
+ }
37
+ }
38
+ result.experts[j] = best_expert;
39
+ result.weights[j] = best_weight;
40
+ }
41
+
42
+ // QMoE without a separate router_weights input always applies softmax over
43
+ // the selected top-k logits, independently of normalize_routing_weights.
44
+ let max_logit = result.weights[0];
45
+ var sum = 0.0;
46
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
47
+ result.weights[j] = exp(result.weights[j] - max_logit);
48
+ sum = sum + result.weights[j];
49
+ }
50
+ if (sum != 0.0) {
51
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
52
+ result.weights[j] = result.weights[j] / sum;
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+
58
+ {% else %}
59
+ // Serial top-1 selection over `(logit, expert_index)` pairs ordered descending,
60
+ // so equal logits select the higher expert index.
61
+ struct Top1Route {
62
+ expert: u32,
63
+ weight: f32,
64
+ }
65
+
66
+ fn top1_route(token: u32) -> Top1Route {
67
+ var result = Top1Route(0u, 1.0);
68
+ var best_weight = router_probs[token * EXPERTS];
69
+ for (var e = 1u; e < EXPERTS; e += 1u) {
70
+ let candidate = router_probs[token * EXPERTS + e];
71
+ if (candidate >= best_weight) {
72
+ result.expert = e;
73
+ best_weight = candidate;
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+
79
+ {% endif %}
80
+
81
+ @compute @workgroup_size(WG, 1, 1)
82
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
83
+ @builtin(num_workgroups) nwg: vec3<u32>) {
84
+ // 2D-folded flat index: gid.y carries the high bits past the maxComputeWorkgroupsPerDimension
85
+ // workgroup-per-dimension dispatch limit. Reduces to gid.x when nwg.y == 1.
86
+ let token = gid.x + gid.y * nwg.x * WG;
87
+ if (token >= TOKENS) {
88
+ return;
89
+ }
90
+
91
+ {% if topK > 1 %}
92
+ // Slot j of each token's TOP_K-wide run holds its j-th ranked expert and
93
+ // that expert's softmax weight within the selected set.
94
+ let route = topk_route(token);
95
+ for (var j = 0u; j < TOP_K; j = j + 1u) {
96
+ route_expert[token * TOP_K + j] = route.experts[j];
97
+ route_mix[token * TOP_K + j] = route.weights[j];
98
+ }
99
+ {% else %}
100
+ let route = top1_route(token);
101
+
102
+ route_expert[token] = route.expert;
103
+ route_mix[token] = route.weight;
104
+ {% endif %}
105
+ }
build/webgpu/test.json ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.QMoE",
3
+ "cases": [
4
+ {
5
+ "name": "q4_colwise_default_relu",
6
+ "provenance": {
7
+ "notes": "Exercises every exact default at once: 4-bit integer weights, k=1, ReLU, column-wise scales, dense routing, and raw provider layout."
8
+ },
9
+ "inputs": {
10
+ "inputT": {
11
+ "dtype": "float32",
12
+ "shape": [2, 4],
13
+ "data": { "kind": "values", "values": [1.0, -2.0, 0.5, 3.0, -1.0, 2.0, 4.0, -0.5] }
14
+ },
15
+ "routerT": {
16
+ "dtype": "float32",
17
+ "shape": [2, 3],
18
+ "data": { "kind": "values", "values": [0.1, 1.2, -0.4, 2.0, -1.0, 0.5] }
19
+ },
20
+ "fc1T": {
21
+ "dtype": "uint8",
22
+ "shape": [3, 4, 2],
23
+ "data": { "kind": "cycle", "values": [154, 104, 124, 185, 39, 211, 78] }
24
+ },
25
+ "fc1ScalesT": { "dtype": "float32", "shape": [3, 4], "data": { "kind": "linspace", "start": 0.05, "end": 0.3 } },
26
+ "fc2T": {
27
+ "dtype": "uint8",
28
+ "shape": [3, 4, 2],
29
+ "data": { "kind": "cycle", "values": [78, 211, 39, 185, 124, 104, 154] }
30
+ },
31
+ "fc2ScalesT": {
32
+ "dtype": "float32",
33
+ "shape": [3, 4],
34
+ "data": { "kind": "linspace", "start": 0.04, "end": 0.22 }
35
+ }
36
+ },
37
+ "outputs": { "outputT": { "dtype": "float32", "shape": [2, 4], "tolerance": 0.00002 } }
38
+ },
39
+ {
40
+ "name": "q4_top1_tie_selects_higher_expert",
41
+ "provenance": {
42
+ "notes": "Pins ONNX Runtime's descending `(logit, expert_index)` ordering with selected-weight normalization: equal logits select expert 1, whose hand-computed output is [16, 32], rather than the all-zero expert 0."
43
+ },
44
+ "attrs": { "normalize_routing_weights": 1 },
45
+ "inputs": {
46
+ "inputT": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [1.0, 1.0] } },
47
+ "routerT": { "dtype": "float32", "shape": [1, 2], "data": { "kind": "values", "values": [0.0, 0.0] } },
48
+ "fc1T": { "dtype": "uint8", "shape": [2, 2, 1], "data": { "kind": "values", "values": [136, 136, 170, 170] } },
49
+ "fc1ScalesT": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "constant", "value": 1.0 } },
50
+ "fc2T": { "dtype": "uint8", "shape": [2, 2, 1], "data": { "kind": "values", "values": [136, 136, 170, 204] } },
51
+ "fc2ScalesT": { "dtype": "float32", "shape": [2, 2], "data": { "kind": "constant", "value": 1.0 } }
52
+ },
53
+ "outputs": {
54
+ "outputT": {
55
+ "dtype": "float32",
56
+ "shape": [1, 2],
57
+ "data": { "kind": "values", "values": [16.0, 32.0] },
58
+ "tolerance": 0
59
+ }
60
+ }
61
+ },
62
+ {
63
+ "name": "q8_colwise_swiglu",
64
+ "provenance": {
65
+ "notes": "Covers 8-bit integer weights and the supported interleaved SwiGLU mode with non-default alpha and beta."
66
+ },
67
+ "attrs": {
68
+ "activation_alpha": 1.1,
69
+ "activation_beta": 0.05,
70
+ "activation_type": "swiglu",
71
+ "expert_weight_bits": 8,
72
+ "swiglu_fusion": 1
73
+ },
74
+ "inputs": {
75
+ "inputT": {
76
+ "dtype": "float32",
77
+ "shape": [3, 4],
78
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.3 }
79
+ },
80
+ "routerT": {
81
+ "dtype": "float32",
82
+ "shape": [3, 3],
83
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.19, "scale": 0.7 }
84
+ },
85
+ "fc1T": {
86
+ "dtype": "uint8",
87
+ "shape": [3, 6, 4],
88
+ "data": { "kind": "cycle", "values": [131, 96, 174, 118, 205, 83, 149] }
89
+ },
90
+ "fc1ScalesT": {
91
+ "dtype": "float32",
92
+ "shape": [3, 6],
93
+ "data": { "kind": "linspace", "start": 0.006, "end": 0.042 }
94
+ },
95
+ "fc2T": {
96
+ "dtype": "uint8",
97
+ "shape": [3, 4, 3],
98
+ "data": { "kind": "cycle", "values": [149, 83, 205, 118, 174, 96, 131] }
99
+ },
100
+ "fc2ScalesT": {
101
+ "dtype": "float32",
102
+ "shape": [3, 4],
103
+ "data": { "kind": "linspace", "start": 0.008, "end": 0.037 }
104
+ }
105
+ },
106
+ "outputs": { "outputT": { "dtype": "float32", "shape": [3, 4], "tolerance": 0.00002 } }
107
+ },
108
+ {
109
+ "name": "q4_blockwise_topk2_swiglu",
110
+ "provenance": {
111
+ "notes": "Two complete 16-element quantization blocks on both reductions, with two selected experts and stable top-k softmax mixing."
112
+ },
113
+ "attrs": {
114
+ "activation_type": "swiglu",
115
+ "block_size": 16,
116
+ "k": 2,
117
+ "normalize_routing_weights": 1,
118
+ "swiglu_fusion": 1,
119
+ "weights_prepacked": 0
120
+ },
121
+ "inputs": {
122
+ "inputT": {
123
+ "dtype": "float32",
124
+ "shape": [4, 32],
125
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.37, "scale": 0.4 }
126
+ },
127
+ "routerT": {
128
+ "dtype": "float32",
129
+ "shape": [4, 3],
130
+ "data": { "kind": "fillFloat32", "sinStep": 0.43, "cosStep": 0.23, "scale": 0.6 }
131
+ },
132
+ "fc1T": {
133
+ "dtype": "uint8",
134
+ "shape": [3, 64, 16],
135
+ "data": { "kind": "cycle", "values": [154, 104, 124, 185, 39, 211, 78] }
136
+ },
137
+ "fc1ScalesT": {
138
+ "dtype": "float32",
139
+ "shape": [3, 64, 2],
140
+ "data": { "kind": "linspace", "start": 0.04, "end": 0.33 }
141
+ },
142
+ "fc2T": {
143
+ "dtype": "uint8",
144
+ "shape": [3, 32, 16],
145
+ "data": { "kind": "cycle", "values": [78, 211, 39, 185, 124, 104, 154] }
146
+ },
147
+ "fc2ScalesT": {
148
+ "dtype": "float32",
149
+ "shape": [3, 32, 2],
150
+ "data": { "kind": "linspace", "start": 0.06, "end": 0.27 }
151
+ }
152
+ },
153
+ "outputs": { "outputT": { "dtype": "float32", "shape": [4, 32], "tolerance": 0.00005 } }
154
+ },
155
+ {
156
+ "name": "q4_rank3_input",
157
+ "provenance": {
158
+ "notes": "Locks the standard rank-3 input form and flattening of batch and sequence into router rows."
159
+ },
160
+ "inputs": {
161
+ "inputT": {
162
+ "dtype": "float32",
163
+ "shape": [2, 2, 4],
164
+ "data": { "kind": "fillFloat32", "sinStep": 0.21, "cosStep": 0.13, "scale": 0.4 }
165
+ },
166
+ "routerT": {
167
+ "dtype": "float32",
168
+ "shape": [4, 2],
169
+ "data": { "kind": "fillFloat32", "sinStep": 0.41, "cosStep": 0.17, "scale": 0.8 }
170
+ },
171
+ "fc1T": { "dtype": "uint8", "shape": [2, 4, 2], "data": { "kind": "cycle", "values": [25, 118, 203, 71, 154] } },
172
+ "fc1ScalesT": { "dtype": "float32", "shape": [2, 4], "data": { "kind": "linspace", "start": 0.04, "end": 0.2 } },
173
+ "fc2T": { "dtype": "uint8", "shape": [2, 4, 2], "data": { "kind": "cycle", "values": [97, 44, 218, 131, 76] } },
174
+ "fc2ScalesT": {
175
+ "dtype": "float32",
176
+ "shape": [2, 4],
177
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.18 }
178
+ }
179
+ },
180
+ "outputs": { "outputT": { "dtype": "float32", "shape": [2, 2, 4], "tolerance": 0.00002 } }
181
+ },
182
+ {
183
+ "name": "q8_swiglu_limit",
184
+ "provenance": { "notes": "Exercises the optional finite SwiGLU clamp and the explicit 8-bit path." },
185
+ "attrs": {
186
+ "activation_alpha": 0.9,
187
+ "activation_beta": -0.02,
188
+ "activation_type": "swiglu",
189
+ "expert_weight_bits": 8,
190
+ "swiglu_fusion": 1,
191
+ "swiglu_limit": 0.75
192
+ },
193
+ "inputs": {
194
+ "inputT": {
195
+ "dtype": "float32",
196
+ "shape": [2, 4],
197
+ "data": { "kind": "values", "values": [2.0, -3.0, 1.0, 4.0, -1.0, 0.5, 3.0, -2.0] }
198
+ },
199
+ "routerT": {
200
+ "dtype": "float32",
201
+ "shape": [2, 2],
202
+ "data": { "kind": "values", "values": [1.0, 0.0, -1.0, 2.0] }
203
+ },
204
+ "fc1T": {
205
+ "dtype": "uint8",
206
+ "shape": [2, 8, 4],
207
+ "data": { "kind": "cycle", "values": [131, 96, 174, 118, 205, 83, 149] }
208
+ },
209
+ "fc1ScalesT": {
210
+ "dtype": "float32",
211
+ "shape": [2, 8],
212
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.12 }
213
+ },
214
+ "fc2T": {
215
+ "dtype": "uint8",
216
+ "shape": [2, 4, 4],
217
+ "data": { "kind": "cycle", "values": [149, 83, 205, 118, 174, 96, 131] }
218
+ },
219
+ "fc2ScalesT": {
220
+ "dtype": "float32",
221
+ "shape": [2, 4],
222
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.11 }
223
+ }
224
+ },
225
+ "outputs": { "outputT": { "dtype": "float32", "shape": [2, 4], "tolerance": 0.00002 } }
226
+ },
227
+ {
228
+ "name": "q4_zero_inter",
229
+ "provenance": {
230
+ "notes": "A zero intermediate dimension has an exact zero result without binding empty weight buffers in a shader."
231
+ },
232
+ "attrs": { "activation_type": "swiglu", "swiglu_fusion": 1 },
233
+ "inputs": {
234
+ "inputT": {
235
+ "dtype": "float32",
236
+ "shape": [3, 4],
237
+ "data": { "kind": "fillFloat32", "sinStep": 0.17, "cosStep": 0.31, "scale": 0.3 }
238
+ },
239
+ "routerT": {
240
+ "dtype": "float32",
241
+ "shape": [3, 3],
242
+ "data": { "kind": "fillFloat32", "sinStep": 0.37, "cosStep": 0.19, "scale": 0.7 }
243
+ },
244
+ "fc1T": { "dtype": "uint8", "shape": [3, 0, 2], "data": { "kind": "values", "values": [] } },
245
+ "fc1ScalesT": { "dtype": "float32", "shape": [3, 0], "data": { "kind": "values", "values": [] } },
246
+ "fc2T": { "dtype": "uint8", "shape": [3, 4, 0], "data": { "kind": "values", "values": [] } },
247
+ "fc2ScalesT": {
248
+ "dtype": "float32",
249
+ "shape": [3, 4],
250
+ "data": { "kind": "linspace", "start": 0.008, "end": 0.037 }
251
+ }
252
+ },
253
+ "outputs": {
254
+ "outputT": { "dtype": "float32", "shape": [3, 4], "data": { "kind": "constant", "value": 0.0 }, "tolerance": 0 }
255
+ }
256
+ },
257
+ {
258
+ "name": "empty_zero_dim",
259
+ "provenance": {
260
+ "notes": "Zero tokens in the rank-2 form must retain the routed quantized shader identities while every output dispatch is elided."
261
+ },
262
+ "inputs": {
263
+ "inputT": { "dtype": "float32", "shape": [0, 4], "data": { "kind": "values", "values": [] } },
264
+ "routerT": { "dtype": "float32", "shape": [0, 3], "data": { "kind": "values", "values": [] } },
265
+ "fc1T": { "dtype": "uint8", "shape": [3, 4, 2], "data": { "kind": "constant", "value": 120 } },
266
+ "fc1ScalesT": { "dtype": "float32", "shape": [3, 4], "data": { "kind": "constant", "value": 0.05 } },
267
+ "fc2T": { "dtype": "uint8", "shape": [3, 4, 2], "data": { "kind": "constant", "value": 130 } },
268
+ "fc2ScalesT": { "dtype": "float32", "shape": [3, 4], "data": { "kind": "constant", "value": 0.05 } }
269
+ },
270
+ "outputs": {
271
+ "outputT": { "dtype": "float32", "shape": [0, 4], "data": { "kind": "values", "values": [] }, "tolerance": 0 }
272
+ }
273
+ },
274
+ {
275
+ "name": "q4_colwise_relu_deep_reduction",
276
+ "provenance": {
277
+ "notes": "A 128-long reduction on both projections, deep enough that splitting it across lanes is worthwhile; the shallow cases above keep the one-thread-per-column schedule."
278
+ },
279
+ "attrs": { "weights_prepacked": 0 },
280
+ "inputs": {
281
+ "inputT": {
282
+ "dtype": "float32",
283
+ "shape": [2, 128],
284
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.4 }
285
+ },
286
+ "routerT": {
287
+ "dtype": "float32",
288
+ "shape": [2, 2],
289
+ "data": { "kind": "fillFloat32", "sinStep": 0.41, "cosStep": 0.17, "scale": 0.6 }
290
+ },
291
+ "fc1T": {
292
+ "dtype": "uint8",
293
+ "shape": [2, 128, 64],
294
+ "data": { "kind": "cycle", "values": [154, 104, 124, 185, 39, 211, 78] }
295
+ },
296
+ "fc1ScalesT": {
297
+ "dtype": "float32",
298
+ "shape": [2, 128],
299
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.19 }
300
+ },
301
+ "fc2T": {
302
+ "dtype": "uint8",
303
+ "shape": [2, 128, 64],
304
+ "data": { "kind": "cycle", "values": [78, 211, 39, 185, 124, 104, 154] }
305
+ },
306
+ "fc2ScalesT": {
307
+ "dtype": "float32",
308
+ "shape": [2, 128],
309
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.21 }
310
+ }
311
+ },
312
+ "outputs": { "outputT": { "dtype": "float32", "shape": [2, 128], "tolerance": 0.00005 } }
313
+ },
314
+ {
315
+ "name": "q4_blockwise_swiglu_topk2_deep_reduction",
316
+ "provenance": {
317
+ "notes": "Interleaved SwiGLU with two selected experts over a 128-long reduction and four 32-wide quantization blocks per row."
318
+ },
319
+ "attrs": {
320
+ "activation_type": "swiglu",
321
+ "block_size": 32,
322
+ "k": 2,
323
+ "normalize_routing_weights": 1,
324
+ "swiglu_fusion": 1,
325
+ "weights_prepacked": 0
326
+ },
327
+ "inputs": {
328
+ "inputT": {
329
+ "dtype": "float32",
330
+ "shape": [3, 128],
331
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.37, "scale": 0.4 }
332
+ },
333
+ "routerT": {
334
+ "dtype": "float32",
335
+ "shape": [3, 3],
336
+ "data": { "kind": "fillFloat32", "sinStep": 0.43, "cosStep": 0.23, "scale": 0.6 }
337
+ },
338
+ "fc1T": {
339
+ "dtype": "uint8",
340
+ "shape": [3, 256, 64],
341
+ "data": { "kind": "cycle", "values": [97, 182, 45, 213, 131, 68, 159, 22, 241] }
342
+ },
343
+ "fc1ScalesT": {
344
+ "dtype": "float32",
345
+ "shape": [3, 256, 4],
346
+ "data": { "kind": "linspace", "start": 0.04, "end": 0.33 }
347
+ },
348
+ "fc2T": {
349
+ "dtype": "uint8",
350
+ "shape": [3, 128, 64],
351
+ "data": { "kind": "cycle", "values": [241, 22, 159, 68, 131, 213, 45, 182, 97] }
352
+ },
353
+ "fc2ScalesT": {
354
+ "dtype": "float32",
355
+ "shape": [3, 128, 4],
356
+ "data": { "kind": "linspace", "start": 0.06, "end": 0.27 }
357
+ }
358
+ },
359
+ "outputs": { "outputT": { "dtype": "float32", "shape": [3, 128], "tolerance": 0.00005 } }
360
+ },
361
+ {
362
+ "name": "q8_swiglu_deep_reduction",
363
+ "provenance": {
364
+ "notes": "Eight-bit codes over the same 128-long reduction: one stored word per logical column instead of a shared nibble pair."
365
+ },
366
+ "attrs": { "activation_type": "swiglu", "expert_weight_bits": 8, "swiglu_fusion": 1, "weights_prepacked": 0 },
367
+ "inputs": {
368
+ "inputT": {
369
+ "dtype": "float32",
370
+ "shape": [2, 128],
371
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.11, "scale": 0.35 }
372
+ },
373
+ "routerT": {
374
+ "dtype": "float32",
375
+ "shape": [2, 2],
376
+ "data": { "kind": "fillFloat32", "sinStep": 0.31, "cosStep": 0.47, "scale": 0.6 }
377
+ },
378
+ "fc1T": {
379
+ "dtype": "uint8",
380
+ "shape": [2, 256, 128],
381
+ "data": { "kind": "cycle", "values": [113, 26, 201, 88, 167, 54, 229, 141, 73] }
382
+ },
383
+ "fc1ScalesT": {
384
+ "dtype": "float32",
385
+ "shape": [2, 256],
386
+ "data": { "kind": "linspace", "start": 0.01, "end": 0.09 }
387
+ },
388
+ "fc2T": {
389
+ "dtype": "uint8",
390
+ "shape": [2, 128, 128],
391
+ "data": { "kind": "cycle", "values": [73, 141, 229, 54, 167, 88, 201, 26, 113] }
392
+ },
393
+ "fc2ScalesT": {
394
+ "dtype": "float32",
395
+ "shape": [2, 128],
396
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.08 }
397
+ }
398
+ },
399
+ "outputs": { "outputT": { "dtype": "float32", "shape": [2, 128], "tolerance": 0.00005 } }
400
+ },
401
+ {
402
+ "name": "q8_swiglu_decode_gemv_odd_reduction",
403
+ "provenance": {
404
+ "notes": "An 8-bit decode on the cooperative GEMV schedule with an ODD hidden size. The reduction steps two columns at a time -- one stored word per column at 8 bits, one word for both at 4 -- so an odd length leaves a final column that only 8-bit weights can produce."
405
+ },
406
+ "attrs": { "activation_type": "swiglu", "expert_weight_bits": 8, "swiglu_fusion": 1, "weights_prepacked": 0 },
407
+ "inputs": {
408
+ "inputT": {
409
+ "dtype": "float32",
410
+ "shape": [1, 129],
411
+ "data": { "kind": "fillFloat32", "sinStep": 0.261, "cosStep": 0.11, "scale": 0.35 }
412
+ },
413
+ "routerT": {
414
+ "dtype": "float32",
415
+ "shape": [1, 2],
416
+ "data": { "kind": "fillFloat32", "sinStep": 0.341, "cosStep": 0.47, "scale": 0.6 }
417
+ },
418
+ "fc1T": {
419
+ "dtype": "uint8",
420
+ "shape": [2, 256, 129],
421
+ "data": { "kind": "cycle", "values": [113, 26, 201, 88, 167, 54, 229, 141, 73] }
422
+ },
423
+ "fc1ScalesT": {
424
+ "dtype": "float32",
425
+ "shape": [2, 256],
426
+ "data": { "kind": "linspace", "start": 0.01, "end": 0.09 }
427
+ },
428
+ "fc2T": {
429
+ "dtype": "uint8",
430
+ "shape": [2, 129, 128],
431
+ "data": { "kind": "cycle", "values": [73, 141, 229, 54, 167, 88, 201, 26, 113] }
432
+ },
433
+ "fc2ScalesT": {
434
+ "dtype": "float32",
435
+ "shape": [2, 129],
436
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.08 }
437
+ }
438
+ },
439
+ "outputs": { "outputT": { "dtype": "float32", "shape": [1, 129], "tolerance": 0.00005 } }
440
+ },
441
+ {
442
+ "name": "q4_grouped_prefill_relu",
443
+ "provenance": {
444
+ "notes": "Enough routed slots per expert to fill grouped tiles, so one dequantized weight tile is reused across a tile of slots."
445
+ },
446
+ "attrs": { "weights_prepacked": 0 },
447
+ "inputs": {
448
+ "inputT": {
449
+ "dtype": "float32",
450
+ "shape": [96, 64],
451
+ "data": { "kind": "fillFloat32", "sinStep": 0.13, "cosStep": 0.29, "scale": 0.4 }
452
+ },
453
+ "routerT": {
454
+ "dtype": "float32",
455
+ "shape": [96, 3],
456
+ "data": { "kind": "fillFloat32", "sinStep": 0.33999999999999997, "cosStep": 0.24, "scale": 0.6 }
457
+ },
458
+ "fc1T": {
459
+ "dtype": "uint8",
460
+ "shape": [3, 64, 32],
461
+ "data": { "kind": "cycle", "values": [97, 182, 45, 213, 131, 68, 159, 22, 241] }
462
+ },
463
+ "fc1ScalesT": {
464
+ "dtype": "float32",
465
+ "shape": [3, 64],
466
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.21 }
467
+ },
468
+ "fc2T": {
469
+ "dtype": "uint8",
470
+ "shape": [3, 64, 32],
471
+ "data": { "kind": "cycle", "values": [241, 22, 159, 68, 131, 213, 45, 182, 97] }
472
+ },
473
+ "fc2ScalesT": {
474
+ "dtype": "float32",
475
+ "shape": [3, 64],
476
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.18 }
477
+ }
478
+ },
479
+ "outputs": { "outputT": { "dtype": "float32", "shape": [96, 64], "tolerance": 0.00005 } }
480
+ },
481
+ {
482
+ "name": "q4_grouped_prefill_swiglu_topk2",
483
+ "provenance": {
484
+ "notes": "Enough routed slots per expert to fill grouped tiles, so one dequantized weight tile is reused across a tile of slots."
485
+ },
486
+ "attrs": {
487
+ "activation_type": "swiglu",
488
+ "k": 2,
489
+ "normalize_routing_weights": 1,
490
+ "swiglu_fusion": 1,
491
+ "weights_prepacked": 0
492
+ },
493
+ "inputs": {
494
+ "inputT": {
495
+ "dtype": "float32",
496
+ "shape": [48, 64],
497
+ "data": { "kind": "fillFloat32", "sinStep": 0.19, "cosStep": 0.37, "scale": 0.4 }
498
+ },
499
+ "routerT": {
500
+ "dtype": "float32",
501
+ "shape": [48, 3],
502
+ "data": { "kind": "fillFloat32", "sinStep": 0.4, "cosStep": 0.32, "scale": 0.6 }
503
+ },
504
+ "fc1T": {
505
+ "dtype": "uint8",
506
+ "shape": [3, 128, 32],
507
+ "data": { "kind": "cycle", "values": [97, 182, 45, 213, 131, 68, 159, 22, 241] }
508
+ },
509
+ "fc1ScalesT": {
510
+ "dtype": "float32",
511
+ "shape": [3, 128],
512
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.21 }
513
+ },
514
+ "fc2T": {
515
+ "dtype": "uint8",
516
+ "shape": [3, 64, 32],
517
+ "data": { "kind": "cycle", "values": [241, 22, 159, 68, 131, 213, 45, 182, 97] }
518
+ },
519
+ "fc2ScalesT": {
520
+ "dtype": "float32",
521
+ "shape": [3, 64],
522
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.18 }
523
+ }
524
+ },
525
+ "outputs": { "outputT": { "dtype": "float32", "shape": [48, 64], "tolerance": 0.00005 } }
526
+ },
527
+ {
528
+ "name": "q4_grouped_prefill_partial_tiles",
529
+ "provenance": {
530
+ "notes": "Half the routed slots the grouped schedule used to demand: 3 experts share 64 slots against 32-row tiles, so most of every tile is padding and the store guard drops more rows than it keeps."
531
+ },
532
+ "attrs": {
533
+ "activation_type": "swiglu",
534
+ "k": 2,
535
+ "normalize_routing_weights": 1,
536
+ "swiglu_fusion": 1,
537
+ "weights_prepacked": 0
538
+ },
539
+ "inputs": {
540
+ "inputT": {
541
+ "dtype": "float32",
542
+ "shape": [32, 64],
543
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.41, "scale": 0.4 }
544
+ },
545
+ "routerT": {
546
+ "dtype": "float32",
547
+ "shape": [32, 3],
548
+ "data": { "kind": "fillFloat32", "sinStep": 0.34, "cosStep": 0.28, "scale": 0.6 }
549
+ },
550
+ "fc1T": {
551
+ "dtype": "uint8",
552
+ "shape": [3, 128, 32],
553
+ "data": { "kind": "cycle", "values": [97, 182, 45, 213, 131, 68, 159, 22, 241] }
554
+ },
555
+ "fc1ScalesT": {
556
+ "dtype": "float32",
557
+ "shape": [3, 128],
558
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.21 }
559
+ },
560
+ "fc2T": {
561
+ "dtype": "uint8",
562
+ "shape": [3, 64, 32],
563
+ "data": { "kind": "cycle", "values": [241, 22, 159, 68, 131, 213, 45, 182, 97] }
564
+ },
565
+ "fc2ScalesT": {
566
+ "dtype": "float32",
567
+ "shape": [3, 64],
568
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.18 }
569
+ }
570
+ },
571
+ "outputs": { "outputT": { "dtype": "float32", "shape": [32, 64], "tolerance": 0.00005 } }
572
+ },
573
+ {
574
+ "name": "q8_grouped_prefill_swiglu_blockwise",
575
+ "provenance": {
576
+ "notes": "Enough routed slots per expert to fill grouped tiles, so one dequantized weight tile is reused across a tile of slots. The tiled reduction sums in a different order than the reference, so the tolerance carries that f32 regrouping; outputs are order 0.4, which a 10% error would move by 0.04."
577
+ },
578
+ "attrs": {
579
+ "activation_type": "swiglu",
580
+ "block_size": 32,
581
+ "k": 2,
582
+ "normalize_routing_weights": 1,
583
+ "swiglu_fusion": 1,
584
+ "weights_prepacked": 0,
585
+ "expert_weight_bits": 8
586
+ },
587
+ "inputs": {
588
+ "inputT": {
589
+ "dtype": "float32",
590
+ "shape": [48, 64],
591
+ "data": { "kind": "fillFloat32", "sinStep": 0.23, "cosStep": 0.17, "scale": 0.4 }
592
+ },
593
+ "routerT": {
594
+ "dtype": "float32",
595
+ "shape": [48, 3],
596
+ "data": { "kind": "fillFloat32", "sinStep": 0.44, "cosStep": 0.12000000000000001, "scale": 0.6 }
597
+ },
598
+ "fc1T": {
599
+ "dtype": "uint8",
600
+ "shape": [3, 128, 64],
601
+ "data": { "kind": "cycle", "values": [97, 182, 45, 213, 131, 68, 159, 22, 241] }
602
+ },
603
+ "fc1ScalesT": {
604
+ "dtype": "float32",
605
+ "shape": [3, 128, 2],
606
+ "data": { "kind": "linspace", "start": 0.02, "end": 0.21 }
607
+ },
608
+ "fc2T": {
609
+ "dtype": "uint8",
610
+ "shape": [3, 64, 64],
611
+ "data": { "kind": "cycle", "values": [241, 22, 159, 68, 131, 213, 45, 182, 97] }
612
+ },
613
+ "fc2ScalesT": {
614
+ "dtype": "float32",
615
+ "shape": [3, 64, 2],
616
+ "data": { "kind": "linspace", "start": 0.03, "end": 0.18 }
617
+ }
618
+ },
619
+ "outputs": { "outputT": { "dtype": "float32", "shape": [48, 64], "tolerance": 0.0003 } }
620
+ }
621
+ ]
622
+ }