Xenova HF Staff commited on
Commit
6dc5ac4
·
verified ·
1 Parent(s): a56da48

sync 6fdf6301e2bb

Browse files
README.md CHANGED
@@ -75,13 +75,13 @@ Some implementation variants require `subgroup-matrix` and `subgroups`. These ar
75
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
76
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
77
  - [`test.json`](build/webgpu/test.json) — correctness cases
78
- - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
79
  - [`expert-group-slots.wgsl.jinja`](build/webgpu/expert-group-slots.wgsl.jinja)
 
80
  - [`qmoe-fc1-activation-gemv.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-gemv.wgsl.jinja)
81
  - [`qmoe-fc1-activation-grouped-sgmat.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-grouped-sgmat.wgsl.jinja)
82
  - [`qmoe-fc1-activation-grouped.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-grouped.wgsl.jinja)
83
  - [`qmoe-fc1-activation-stage.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-stage.wgsl.jinja)
84
- - [`qmoe-mix-stage.wgsl.jinja`](build/webgpu/qmoe-mix-stage.wgsl.jinja)
85
  - [`qmoe-output-gemv.wgsl.jinja`](build/webgpu/qmoe-output-gemv.wgsl.jinja)
86
  - [`qmoe-output-grouped-sgmat.wgsl.jinja`](build/webgpu/qmoe-output-grouped-sgmat.wgsl.jinja)
87
  - [`qmoe-output-grouped.wgsl.jinja`](build/webgpu/qmoe-output-grouped.wgsl.jinja)
@@ -92,7 +92,7 @@ Some implementation variants require `subgroup-matrix` and `subgroups`. These ar
92
  ## Use with `@huggingface/kernels`
93
 
94
  ```sh
95
- npm install --save-exact @huggingface/kernels@0.0.1-preview.2
96
  ```
97
 
98
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
 
75
  - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
76
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
77
  - [`test.json`](build/webgpu/test.json) — correctness cases
78
+ - [`bench.json`](build/webgpu/bench.json) — benchmark cases
79
  - [`expert-group-slots.wgsl.jinja`](build/webgpu/expert-group-slots.wgsl.jinja)
80
+ - [`expert-slot-mix.wgsl.jinja`](build/webgpu/expert-slot-mix.wgsl.jinja)
81
  - [`qmoe-fc1-activation-gemv.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-gemv.wgsl.jinja)
82
  - [`qmoe-fc1-activation-grouped-sgmat.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-grouped-sgmat.wgsl.jinja)
83
  - [`qmoe-fc1-activation-grouped.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-grouped.wgsl.jinja)
84
  - [`qmoe-fc1-activation-stage.wgsl.jinja`](build/webgpu/qmoe-fc1-activation-stage.wgsl.jinja)
 
85
  - [`qmoe-output-gemv.wgsl.jinja`](build/webgpu/qmoe-output-gemv.wgsl.jinja)
86
  - [`qmoe-output-grouped-sgmat.wgsl.jinja`](build/webgpu/qmoe-output-grouped-sgmat.wgsl.jinja)
87
  - [`qmoe-output-grouped.wgsl.jinja`](build/webgpu/qmoe-output-grouped.wgsl.jinja)
 
92
  ## Use with `@huggingface/kernels`
93
 
94
  ```sh
95
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.3
96
  ```
97
 
98
  Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
build/webgpu/{qmoe-mix-stage.wgsl.jinja → expert-slot-mix.wgsl.jinja} RENAMED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
 
3
  // Routed sum for the grouped schedule. The grouped FC2 stage produces one projected row per
@@ -11,9 +16,7 @@ const WG: u32 = {{ workgroupSize }}u;
11
 
12
  @compute @workgroup_size(WG, 1, 1)
13
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
14
- // 2D-folded flat index: gid.y carries the high bits past the per-axis dispatch fold width.
15
- // Reduces to gid.x when the dispatch does not fold.
16
- let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
17
  let total = params.tokenCount * HIDDEN;
18
  if (index >= total) {
19
  return;
 
1
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
2
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width.
5
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};{% endmacro %}
6
  {{ env.wgsl.resourceDeclarations }}
7
 
8
  // Routed sum for the grouped schedule. The grouped FC2 stage produces one projected row per
 
16
 
17
  @compute @workgroup_size(WG, 1, 1)
18
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
19
+ {{ flat_index_2d("WG", "index", "") }}
 
 
20
  let total = params.tokenCount * HIDDEN;
21
  if (index >= total) {
22
  return;
build/webgpu/manifest.json CHANGED
@@ -52,9 +52,28 @@
52
  "canPinSubgroupSize32": "device.features.has(\"subgroups\") and device.features.has(\"subgroup-size-control\") and has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize <= 32 and device.adapterInfo.subgroupMaxSize >= 32",
53
  "pinSubgroupSize32": "canPinSubgroupSize32 and not wave32Adapter",
54
  "wave32Effective": "wave32Adapter or pinSubgroupSize32",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  "hiddenSize": "dim(shapes.inputT, ranks.inputT - 1)",
56
  "numTokens": "numel(shapes.inputT) / max(1, hiddenSize)",
57
- "topK": "attrs.k",
58
  "weightBits": "attrs.expert_weight_bits",
59
  "quantBlockSize": "attrs.block_size if has(attrs, \"block_size\") else 0",
60
  "packSize": "2 if weightBits == 4 else 1",
@@ -74,19 +93,13 @@
74
  "quantScalesOk": "tensorDtypes.fc1ScalesT == \"float32\" and tensorDtypes.fc2ScalesT == \"float32\" and dim(shapes.fc1ScalesT, 0) == dim(shapes.routerT, 1) and dim(shapes.fc2ScalesT, 0) == dim(shapes.routerT, 1) and dim(shapes.fc1ScalesT, 1) == dim(shapes.fc1T, 1) and dim(shapes.fc2ScalesT, 1) == hiddenSize and ((ranks.fc1ScalesT == 2 and ranks.fc2ScalesT == 2) if colWiseScales else (ranks.fc1ScalesT == 3 and ranks.fc2ScalesT == 3 and dim(shapes.fc1ScalesT, 2) == fc1ScaleBlocks and dim(shapes.fc2ScalesT, 2) == fc2ScaleBlocks))",
75
  "quantShapeOk": "inputOutputShapeOk and ranks.routerT == 2 and ranks.fc1T == 3 and ranks.fc2T == 3 and dim(shapes.routerT, 0) == numTokens and dim(shapes.fc1T, 0) == dim(shapes.routerT, 1) and dim(shapes.fc2T, 0) == dim(shapes.routerT, 1) and dim(shapes.fc1T, 1) % fusionSize == 0 and dim(shapes.fc2T, 1) == hiddenSize and dim(shapes.fc1T, 2) * packSize == hiddenSize and dim(shapes.fc2T, 2) * packSize == interSize and quantBlockSizeOk and quantScalesOk",
76
  "quantContract": "activationSupported and routingModeSupported and rawWeightLayout and quantShapeOk and topK >= 1 and topK <= dim(shapes.routerT, 1)",
77
- "workgroupSizeOk": "tunables.workgroupSize >= 1 and tunables.workgroupSize <= deviceWorkgroupCap",
78
  "hiddenChunkFits": "topK * interSize * 4 <= device.limits.maxStorageBufferBindingSize and topK * interSize * 4 <= device.limits.maxBufferSize",
79
  "hiddenChunkTokens": "numTokens if interSize == 0 else min(numTokens, max(1, floor(min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize) / (topK * interSize * 4))))",
80
  "hiddenChunkCount": "max(1, ceilDiv(numTokens, max(1, hiddenChunkTokens)))",
81
  "routeScratchBytes": "numTokens * topK * 4",
82
  "routedScratchFits": "routeScratchBytes <= device.limits.maxStorageBufferBindingSize and routeScratchBytes <= device.limits.maxBufferSize",
83
- "groupTileM": "tunables.groupThreads * tunables.groupRegM",
84
- "groupTileN": "tunables.groupThreads * tunables.groupRegN",
85
- "groupTileKVec": "ceilDiv(tunables.groupTileK, 4)",
86
- "groupThreadCount": "tunables.groupThreads * tunables.groupThreads",
87
  "groupSlots": "hiddenChunkTokens * topK",
88
  "groupMaxTiles": "ceilDiv(groupSlots, max(1, groupTileM)) + dim(shapes.routerT, 1)",
89
- "groupSharedBytes": "(groupTileM * tunables.groupTileK + 2 * groupTileN * tunables.groupTileK + groupTileM) * 4",
90
  "groupSlotOutBytes": "hiddenChunkTokens * topK * hiddenSize * 4",
91
  "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.routerT, 1) * 8 <= device.limits.maxComputeWorkgroupStorageSize and tunables.groupTileK % 4 == 0",
92
  "groupedDispatchOk": "groupMaxTiles <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(interSize, max(1, groupTileN)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(hiddenSize, max(1, groupTileN)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
@@ -96,43 +109,35 @@
96
  "sgmatWorkgroup": "128",
97
  "sgmatSubgroups": "4",
98
  "sgmatRowSubtiles": "2",
 
99
  "groupedSgmatStageElements": "max(groupTileM * 32, sgmatSubgroups * 4 * 64)",
100
- "groupedSgmatSharedBytes": "(groupedSgmatStageElements + 64 * 32) * 4",
101
- "groupedSgmatOk": "groupedContract and groupTileM == 32 and interSize % 32 == 0 and groupedSgmatSharedBytes <= device.limits.maxComputeWorkgroupStorageSize and ceilDiv(hiddenSize, 64) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and hiddenSize % 32 == 0",
102
- "decodeLanesOk": "tunables.decodeLanes >= 1 and tunables.decodeLanes <= tunables.workgroupSize and tunables.workgroupSize % tunables.decodeLanes == 0",
103
- "decodeRows": "max(1, tunables.workgroupSize / max(1, tunables.decodeLanes))",
104
- "decodeDeviceOk": "workgroupSizeOk and decodeLanesOk and tunables.decodeLanes <= device.limits.maxComputeWorkgroupSizeX and decodeRows <= device.limits.maxComputeWorkgroupSizeY and tunables.workgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
105
  "decodeDispatchOk": "ceilDiv(interSize, decodeRows) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(hiddenSize, decodeRows) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and hiddenChunkTokens * topK <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
106
  "decodeLaneDepth": "min(hiddenSize, interSize) / max(1, tunables.decodeLanes)",
107
  "decodeDepthOk": "decodeLaneDepth >= tunables.decodeMinLaneTrips",
108
  "splitFc1Blocks": "ceilDiv(numTokens * topK * interSize, max(1, tunables.workgroupSize))",
109
  "decodeOccupancyOk": "splitFc1Blocks <= tunables.decodeBlockTarget",
110
  "decodeContract": "quantContract and decodeDeviceOk and decodeDispatchOk and hiddenChunkFits and routedScratchFits and interSize > 0 and decodeDepthOk and (decodeOccupancyOk or not groupedContract)",
111
- "tokens": "numTokens",
112
  "hidden": "hiddenSize",
113
  "experts": "dim(shapes.routerT, 1)",
114
- "fc1Rows": "dim(shapes.fc1T, 1)",
115
- "inter": "interSize",
116
- "workgroupSize": "tunables.workgroupSize",
117
- "activationType": "attrs.activation_type",
118
- "hasSwigluLimit": "has(attrs, \"swiglu_limit\")",
119
- "swigluLimit": "attrs.swiglu_limit if has(attrs, \"swiglu_limit\") else 0"
120
  },
121
  "bindings": {
122
- "output": { "arg": "outputT", "buffer": "storage", "elementType": "f32" },
123
- "router_probs": { "arg": "routerT", "buffer": "read-only-storage", "elementType": "f32" },
124
- "route_expert": { "scratch": "routeExpert", "buffer": "storage", "elementType": "u32" },
125
- "route_mix": { "scratch": "routeMix", "buffer": "storage", "elementType": "f32" },
126
- "route_expert_2": {
127
  "scratch": "routeExpert",
128
  "name": "route_expert",
129
  "buffer": "read-only-storage",
130
  "elementType": "u32"
131
  },
132
- "slot_list": { "scratch": "slotList", "buffer": "storage", "elementType": "u32" },
133
- "tile_meta": { "scratch": "tileMeta", "buffer": "storage", "elementType": "u32" },
 
134
  "params": {
135
- "buffer": "uniform",
136
  "struct": [
137
  { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" },
138
  {
@@ -142,32 +147,31 @@
142
  }
143
  ]
144
  },
145
- "input": { "arg": "inputT", "buffer": "read-only-storage", "elementType": "f32" },
146
- "slot_list_2": { "scratch": "slotList", "name": "slot_list", "buffer": "read-only-storage", "elementType": "u32" },
147
- "tile_meta_2": { "scratch": "tileMeta", "name": "tile_meta", "buffer": "read-only-storage", "elementType": "u32" },
148
- "fc1_experts_weights": { "arg": "fc1T", "buffer": "read-only-storage", "elementType": "u32" },
149
- "fc1_scales": { "arg": "fc1ScalesT", "buffer": "read-only-storage", "elementType": "f32" },
150
- "hidden_act": { "scratch": "hiddenAct", "buffer": "storage", "elementType": "f32" },
151
- "params_2": {
152
  "name": "params",
153
- "buffer": "uniform",
154
  "struct": [
155
  { "name": "activationAlpha", "type": "f32", "value": "attrs.activation_alpha" },
156
  { "name": "activationBeta", "type": "f32", "value": "attrs.activation_beta" },
157
  { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" }
158
  ]
159
  },
160
- "hidden_act_2": {
161
  "scratch": "hiddenAct",
162
  "name": "hidden_act",
163
  "buffer": "read-only-storage",
164
  "elementType": "f32"
165
  },
166
- "fc2_experts_weights": { "arg": "fc2T", "buffer": "read-only-storage", "elementType": "u32" },
167
- "fc2_scales": { "arg": "fc2ScalesT", "buffer": "read-only-storage", "elementType": "f32" },
168
- "slot_out": { "scratch": "slotOut", "buffer": "storage", "elementType": "f32" },
169
- "slot_out_2": { "scratch": "slotOut", "name": "slot_out", "buffer": "read-only-storage", "elementType": "f32" },
170
- "route_mix_2": { "scratch": "routeMix", "name": "route_mix", "buffer": "read-only-storage", "elementType": "f32" }
171
  },
172
  "variants": [
173
  {
@@ -197,7 +201,6 @@
197
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
198
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
199
  },
200
- "derive": { "groupRouteWorkgroup": "tunables.groupRouteWorkgroup" },
201
  "intermediates": [
202
  { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
203
  { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
@@ -211,7 +214,7 @@
211
  "id": "route_stage",
212
  "name": "QMoE.RouteStage",
213
  "shader": "qmoe-route-stage.wgsl.jinja",
214
- "bindings": ["router_probs", "route_expert", "route_mix"],
215
  "dispatch": {
216
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
217
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
@@ -226,32 +229,28 @@
226
  "id": "group_stage",
227
  "name": "QMoE.GroupStage",
228
  "shader": "expert-group-slots.wgsl.jinja",
229
- "bindings": ["route_expert_2", "slot_list", "tile_meta", "params"],
230
- "dispatch": {
231
- "x": "min(ceilDiv((groupRouteWorkgroup), (groupRouteWorkgroup)), 65535)",
232
- "y": "ceilDiv(ceilDiv((groupRouteWorkgroup), (groupRouteWorkgroup)), 65535)",
233
- "z": 1
234
- }
235
  },
236
  {
237
  "id": "fc1_activation_stage",
238
  "name": "QMoE.FC1ActivationStageGroupedSgmat",
239
  "shader": "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja",
240
- "bindings": ["input", "slot_list_2", "tile_meta_2", "fc1_experts_weights", "fc1_scales", "hidden_act", "params_2"],
241
- "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(interSize * fusionSize, 64)" }
242
  },
243
  {
244
  "id": "output_stage",
245
  "name": "QMoE.OutputStageGroupedSgmat",
246
  "shader": "qmoe-output-grouped-sgmat.wgsl.jinja",
247
- "bindings": ["hidden_act_2", "slot_list_2", "tile_meta_2", "fc2_experts_weights", "fc2_scales", "slot_out"],
248
- "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(hiddenSize, 64)" }
249
  },
250
  {
251
  "id": "mix_stage",
252
  "name": "QMoE.MixStage",
253
- "shader": "qmoe-mix-stage.wgsl.jinja",
254
- "bindings": ["slot_out_2", "route_mix_2", "output", "params"],
255
  "dispatch": {
256
  "x": "min(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
257
  "y": "ceilDiv(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
@@ -266,13 +265,6 @@
266
  "id": "quant_grouped_routed",
267
  "priority": 30,
268
  "when": ["groupedContract"],
269
- "derive": {
270
- "groupTileK": "tunables.groupTileK",
271
- "groupThreads": "tunables.groupThreads",
272
- "regM": "tunables.groupRegM",
273
- "regN": "tunables.groupRegN",
274
- "groupRouteWorkgroup": "tunables.groupRouteWorkgroup"
275
- },
276
  "intermediates": [
277
  { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
278
  { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
@@ -286,7 +278,7 @@
286
  "id": "route_stage",
287
  "name": "QMoE.RouteStage",
288
  "shader": "qmoe-route-stage.wgsl.jinja",
289
- "bindings": ["router_probs", "route_expert", "route_mix"],
290
  "dispatch": {
291
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
292
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
@@ -301,32 +293,28 @@
301
  "id": "group_stage",
302
  "name": "QMoE.GroupStage",
303
  "shader": "expert-group-slots.wgsl.jinja",
304
- "bindings": ["route_expert_2", "slot_list", "tile_meta", "params"],
305
- "dispatch": {
306
- "x": "min(ceilDiv((groupRouteWorkgroup), (groupRouteWorkgroup)), 65535)",
307
- "y": "ceilDiv(ceilDiv((groupRouteWorkgroup), (groupRouteWorkgroup)), 65535)",
308
- "z": 1
309
- }
310
  },
311
  {
312
  "id": "fc1_activation_stage",
313
  "name": "QMoE.FC1ActivationStageGrouped",
314
  "shader": "qmoe-fc1-activation-grouped.wgsl.jinja",
315
- "bindings": ["input", "slot_list_2", "tile_meta_2", "fc1_experts_weights", "fc1_scales", "hidden_act", "params_2"],
316
  "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(interSize, groupTileN)" }
317
  },
318
  {
319
  "id": "output_stage",
320
  "name": "QMoE.OutputStageGrouped",
321
  "shader": "qmoe-output-grouped.wgsl.jinja",
322
- "bindings": ["hidden_act_2", "slot_list_2", "tile_meta_2", "fc2_experts_weights", "fc2_scales", "slot_out"],
323
  "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(hiddenSize, groupTileN)" }
324
  },
325
  {
326
  "id": "mix_stage",
327
  "name": "QMoE.MixStage",
328
- "shader": "qmoe-mix-stage.wgsl.jinja",
329
- "bindings": ["slot_out_2", "route_mix_2", "output", "params"],
330
  "dispatch": {
331
  "x": "min(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
332
  "y": "ceilDiv(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
@@ -352,7 +340,7 @@
352
  "id": "route_stage",
353
  "name": "QMoE.RouteStage",
354
  "shader": "qmoe-route-stage.wgsl.jinja",
355
- "bindings": ["router_probs", "route_expert", "route_mix"],
356
  "dispatch": {
357
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
358
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
@@ -367,7 +355,7 @@
367
  "id": "fc1_activation_stage",
368
  "name": "QMoE.FC1ActivationStageGemv",
369
  "shader": "qmoe-fc1-activation-gemv.wgsl.jinja",
370
- "bindings": ["input", "route_expert_2", "fc1_experts_weights", "fc1_scales", "hidden_act", "params_2"],
371
  "dispatch": {
372
  "x": "ceilDiv(interSize, decodeRows)",
373
  "y": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * topK"
@@ -378,9 +366,9 @@
378
  "name": "QMoE.OutputStageGemv",
379
  "shader": "qmoe-output-gemv.wgsl.jinja",
380
  "bindings": [
381
- "hidden_act_2",
382
- "route_expert_2",
383
- "route_mix_2",
384
  "fc2_experts_weights",
385
  "fc2_scales",
386
  "output",
@@ -412,7 +400,7 @@
412
  "id": "route_stage",
413
  "name": "QMoE.RouteStage",
414
  "shader": "qmoe-route-stage.wgsl.jinja",
415
- "bindings": ["router_probs", "route_expert", "route_mix"],
416
  "dispatch": {
417
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
418
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
@@ -429,7 +417,7 @@
429
  "shader": "qmoe-fc1-activation-stage.wgsl.jinja",
430
  "bindings": [
431
  "input",
432
- "route_expert_2",
433
  "fc1_experts_weights",
434
  "fc1_scales",
435
  "hidden_act",
@@ -457,7 +445,7 @@
457
  "id": "output_stage",
458
  "name": "QMoE.OutputStage",
459
  "shader": "qmoe-output-stage.wgsl.jinja",
460
- "bindings": ["hidden_act_2", "route_expert_2", "route_mix_2", "fc2_experts_weights", "fc2_scales", "output", "params"],
461
  "dispatch": {
462
  "x": "min(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
463
  "y": "ceilDiv(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
 
52
  "canPinSubgroupSize32": "device.features.has(\"subgroups\") and device.features.has(\"subgroup-size-control\") and has(device.adapterInfo, \"subgroupMinSize\") and has(device.adapterInfo, \"subgroupMaxSize\") and device.adapterInfo.subgroupMinSize <= 32 and device.adapterInfo.subgroupMaxSize >= 32",
53
  "pinSubgroupSize32": "canPinSubgroupSize32 and not wave32Adapter",
54
  "wave32Effective": "wave32Adapter or pinSubgroupSize32",
55
+ "groupTileK": "tunables.groupTileK",
56
+ "groupThreads": "tunables.groupThreads",
57
+ "regM": "tunables.groupRegM",
58
+ "regN": "tunables.groupRegN",
59
+ "groupRouteWorkgroup": "tunables.groupRouteWorkgroup",
60
+ "groupTileM": "tunables.groupThreads * tunables.groupRegM",
61
+ "groupTileN": "tunables.groupThreads * tunables.groupRegN",
62
+ "groupTileKVec": "ceilDiv(tunables.groupTileK, 4)",
63
+ "groupThreadCount": "tunables.groupThreads * tunables.groupThreads",
64
+ "groupSharedBytes": "(groupTileM * tunables.groupTileK + 2 * groupTileN * tunables.groupTileK + groupTileM) * 4",
65
+ "topK": "attrs.k",
66
+ "activationType": "attrs.activation_type",
67
+ "workgroupSizeOk": "tunables.workgroupSize >= 1 and tunables.workgroupSize <= deviceWorkgroupCap",
68
+ "decodeLanesOk": "tunables.decodeLanes >= 1 and tunables.decodeLanes <= tunables.workgroupSize and tunables.workgroupSize % tunables.decodeLanes == 0",
69
+ "decodeRows": "max(1, tunables.workgroupSize / max(1, tunables.decodeLanes))",
70
+ "decodeDeviceOk": "workgroupSizeOk and decodeLanesOk and tunables.decodeLanes <= device.limits.maxComputeWorkgroupSizeX and decodeRows <= device.limits.maxComputeWorkgroupSizeY and tunables.workgroupSize * 8 <= device.limits.maxComputeWorkgroupStorageSize",
71
+ "fc1Rows": "dim(shapes.fc1T, 1)",
72
+ "hasSwigluLimit": "has(attrs, \"swiglu_limit\")",
73
+ "swigluLimit": "attrs.swiglu_limit if has(attrs, \"swiglu_limit\") else 0",
74
+ "workgroupSize": "tunables.workgroupSize",
75
  "hiddenSize": "dim(shapes.inputT, ranks.inputT - 1)",
76
  "numTokens": "numel(shapes.inputT) / max(1, hiddenSize)",
 
77
  "weightBits": "attrs.expert_weight_bits",
78
  "quantBlockSize": "attrs.block_size if has(attrs, \"block_size\") else 0",
79
  "packSize": "2 if weightBits == 4 else 1",
 
93
  "quantScalesOk": "tensorDtypes.fc1ScalesT == \"float32\" and tensorDtypes.fc2ScalesT == \"float32\" and dim(shapes.fc1ScalesT, 0) == dim(shapes.routerT, 1) and dim(shapes.fc2ScalesT, 0) == dim(shapes.routerT, 1) and dim(shapes.fc1ScalesT, 1) == dim(shapes.fc1T, 1) and dim(shapes.fc2ScalesT, 1) == hiddenSize and ((ranks.fc1ScalesT == 2 and ranks.fc2ScalesT == 2) if colWiseScales else (ranks.fc1ScalesT == 3 and ranks.fc2ScalesT == 3 and dim(shapes.fc1ScalesT, 2) == fc1ScaleBlocks and dim(shapes.fc2ScalesT, 2) == fc2ScaleBlocks))",
94
  "quantShapeOk": "inputOutputShapeOk and ranks.routerT == 2 and ranks.fc1T == 3 and ranks.fc2T == 3 and dim(shapes.routerT, 0) == numTokens and dim(shapes.fc1T, 0) == dim(shapes.routerT, 1) and dim(shapes.fc2T, 0) == dim(shapes.routerT, 1) and dim(shapes.fc1T, 1) % fusionSize == 0 and dim(shapes.fc2T, 1) == hiddenSize and dim(shapes.fc1T, 2) * packSize == hiddenSize and dim(shapes.fc2T, 2) * packSize == interSize and quantBlockSizeOk and quantScalesOk",
95
  "quantContract": "activationSupported and routingModeSupported and rawWeightLayout and quantShapeOk and topK >= 1 and topK <= dim(shapes.routerT, 1)",
 
96
  "hiddenChunkFits": "topK * interSize * 4 <= device.limits.maxStorageBufferBindingSize and topK * interSize * 4 <= device.limits.maxBufferSize",
97
  "hiddenChunkTokens": "numTokens if interSize == 0 else min(numTokens, max(1, floor(min(device.limits.maxStorageBufferBindingSize, device.limits.maxBufferSize) / (topK * interSize * 4))))",
98
  "hiddenChunkCount": "max(1, ceilDiv(numTokens, max(1, hiddenChunkTokens)))",
99
  "routeScratchBytes": "numTokens * topK * 4",
100
  "routedScratchFits": "routeScratchBytes <= device.limits.maxStorageBufferBindingSize and routeScratchBytes <= device.limits.maxBufferSize",
 
 
 
 
101
  "groupSlots": "hiddenChunkTokens * topK",
102
  "groupMaxTiles": "ceilDiv(groupSlots, max(1, groupTileM)) + dim(shapes.routerT, 1)",
 
103
  "groupSlotOutBytes": "hiddenChunkTokens * topK * hiddenSize * 4",
104
  "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.routerT, 1) * 8 <= device.limits.maxComputeWorkgroupStorageSize and tunables.groupTileK % 4 == 0",
105
  "groupedDispatchOk": "groupMaxTiles <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(interSize, max(1, groupTileN)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(hiddenSize, max(1, groupTileN)) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
 
109
  "sgmatWorkgroup": "128",
110
  "sgmatSubgroups": "4",
111
  "sgmatRowSubtiles": "2",
112
+ "sgmatTileCols": "64",
113
  "groupedSgmatStageElements": "max(groupTileM * 32, sgmatSubgroups * 4 * 64)",
114
+ "groupedSgmatSharedBytes": "(groupedSgmatStageElements + sgmatTileCols * 32) * 4",
115
+ "groupedSgmatOk": "groupedContract and groupTileM == 32 and interSize % 32 == 0 and groupedSgmatSharedBytes <= device.limits.maxComputeWorkgroupStorageSize and ceilDiv(hiddenSize, sgmatTileCols) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and hiddenSize % 32 == 0",
 
 
 
116
  "decodeDispatchOk": "ceilDiv(interSize, decodeRows) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and ceilDiv(hiddenSize, decodeRows) <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535) and hiddenChunkTokens * topK <= min(device.limits.maxComputeWorkgroupsPerDimension, 65535)",
117
  "decodeLaneDepth": "min(hiddenSize, interSize) / max(1, tunables.decodeLanes)",
118
  "decodeDepthOk": "decodeLaneDepth >= tunables.decodeMinLaneTrips",
119
  "splitFc1Blocks": "ceilDiv(numTokens * topK * interSize, max(1, tunables.workgroupSize))",
120
  "decodeOccupancyOk": "splitFc1Blocks <= tunables.decodeBlockTarget",
121
  "decodeContract": "quantContract and decodeDeviceOk and decodeDispatchOk and hiddenChunkFits and routedScratchFits and interSize > 0 and decodeDepthOk and (decodeOccupancyOk or not groupedContract)",
 
122
  "hidden": "hiddenSize",
123
  "experts": "dim(shapes.routerT, 1)",
124
+ "inter": "interSize"
 
 
 
 
 
125
  },
126
  "bindings": {
127
+ "output": { "arg": "outputT", "elementType": "f32" },
128
+ "router_probs": { "arg": "routerT", "elementType": "f32" },
129
+ "route_expert": { "scratch": "routeExpert", "elementType": "u32" },
130
+ "route_mix": { "scratch": "routeMix", "elementType": "f32" },
131
+ "route_expert_u32": {
132
  "scratch": "routeExpert",
133
  "name": "route_expert",
134
  "buffer": "read-only-storage",
135
  "elementType": "u32"
136
  },
137
+ "slot_list": { "scratch": "slotList", "elementType": "u32" },
138
+ "tile_meta": { "scratch": "tileMeta", "elementType": "u32" },
139
+ "params_route": { "name": "params", "struct": [{ "name": "tokenCount", "type": "u32", "value": "numTokens" }] },
140
  "params": {
 
141
  "struct": [
142
  { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" },
143
  {
 
147
  }
148
  ]
149
  },
150
+ "input": { "arg": "inputT", "elementType": "f32" },
151
+ "slot_list_u32": { "scratch": "slotList", "name": "slot_list", "buffer": "read-only-storage", "elementType": "u32" },
152
+ "tile_meta_u32": { "scratch": "tileMeta", "name": "tile_meta", "buffer": "read-only-storage", "elementType": "u32" },
153
+ "fc1_experts_weights": { "arg": "fc1T", "elementType": "u32" },
154
+ "fc1_scales": { "arg": "fc1ScalesT", "elementType": "f32" },
155
+ "hidden_act": { "scratch": "hiddenAct", "elementType": "f32" },
156
+ "params__uniform": {
157
  "name": "params",
 
158
  "struct": [
159
  { "name": "activationAlpha", "type": "f32", "value": "attrs.activation_alpha" },
160
  { "name": "activationBeta", "type": "f32", "value": "attrs.activation_beta" },
161
  { "name": "tokenOffset", "type": "u32", "value": "repeat.chunk * hiddenChunkTokens" }
162
  ]
163
  },
164
+ "hidden_act_f32": {
165
  "scratch": "hiddenAct",
166
  "name": "hidden_act",
167
  "buffer": "read-only-storage",
168
  "elementType": "f32"
169
  },
170
+ "fc2_experts_weights": { "arg": "fc2T", "elementType": "u32" },
171
+ "fc2_scales": { "arg": "fc2ScalesT", "elementType": "f32" },
172
+ "slot_out": { "scratch": "slotOut", "elementType": "f32" },
173
+ "slot_out_f32": { "scratch": "slotOut", "name": "slot_out", "buffer": "read-only-storage", "elementType": "f32" },
174
+ "route_mix_f32": { "scratch": "routeMix", "name": "route_mix", "buffer": "read-only-storage", "elementType": "f32" }
175
  },
176
  "variants": [
177
  {
 
201
  "features": ["subgroups", "chromium-experimental-subgroup-matrix"],
202
  "subgroupMatrixConfigs": [{ "componentType": "f32", "resultComponentType": "f32", "M": 8, "N": 8, "K": 8 }]
203
  },
 
204
  "intermediates": [
205
  { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
206
  { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
 
214
  "id": "route_stage",
215
  "name": "QMoE.RouteStage",
216
  "shader": "qmoe-route-stage.wgsl.jinja",
217
+ "bindings": ["router_probs", "route_expert", "route_mix", "params_route"],
218
  "dispatch": {
219
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
220
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
 
229
  "id": "group_stage",
230
  "name": "QMoE.GroupStage",
231
  "shader": "expert-group-slots.wgsl.jinja",
232
+ "bindings": ["route_expert_u32", "slot_list", "tile_meta", "params"],
233
+ "dispatch": { "x": 1 }
 
 
 
 
234
  },
235
  {
236
  "id": "fc1_activation_stage",
237
  "name": "QMoE.FC1ActivationStageGroupedSgmat",
238
  "shader": "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja",
239
+ "bindings": ["input", "slot_list_u32", "tile_meta_u32", "fc1_experts_weights", "fc1_scales", "hidden_act", "params__uniform"],
240
+ "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(interSize * fusionSize, sgmatTileCols)" }
241
  },
242
  {
243
  "id": "output_stage",
244
  "name": "QMoE.OutputStageGroupedSgmat",
245
  "shader": "qmoe-output-grouped-sgmat.wgsl.jinja",
246
+ "bindings": ["hidden_act_f32", "slot_list_u32", "tile_meta_u32", "fc2_experts_weights", "fc2_scales", "slot_out"],
247
+ "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(hiddenSize, sgmatTileCols)" }
248
  },
249
  {
250
  "id": "mix_stage",
251
  "name": "QMoE.MixStage",
252
+ "shader": "expert-slot-mix.wgsl.jinja",
253
+ "bindings": ["slot_out_f32", "route_mix_f32", "output", "params"],
254
  "dispatch": {
255
  "x": "min(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
256
  "y": "ceilDiv(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
 
265
  "id": "quant_grouped_routed",
266
  "priority": 30,
267
  "when": ["groupedContract"],
 
 
 
 
 
 
 
268
  "intermediates": [
269
  { "id": "routeExpert", "dtype": "uint32", "shape": "[numTokens * topK]" },
270
  { "id": "routeMix", "dtype": "float32", "shape": "[numTokens * topK]" },
 
278
  "id": "route_stage",
279
  "name": "QMoE.RouteStage",
280
  "shader": "qmoe-route-stage.wgsl.jinja",
281
+ "bindings": ["router_probs", "route_expert", "route_mix", "params_route"],
282
  "dispatch": {
283
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
284
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
 
293
  "id": "group_stage",
294
  "name": "QMoE.GroupStage",
295
  "shader": "expert-group-slots.wgsl.jinja",
296
+ "bindings": ["route_expert_u32", "slot_list", "tile_meta", "params"],
297
+ "dispatch": { "x": 1 }
 
 
 
 
298
  },
299
  {
300
  "id": "fc1_activation_stage",
301
  "name": "QMoE.FC1ActivationStageGrouped",
302
  "shader": "qmoe-fc1-activation-grouped.wgsl.jinja",
303
+ "bindings": ["input", "slot_list_u32", "tile_meta_u32", "fc1_experts_weights", "fc1_scales", "hidden_act", "params__uniform"],
304
  "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(interSize, groupTileN)" }
305
  },
306
  {
307
  "id": "output_stage",
308
  "name": "QMoE.OutputStageGrouped",
309
  "shader": "qmoe-output-grouped.wgsl.jinja",
310
+ "bindings": ["hidden_act_f32", "slot_list_u32", "tile_meta_u32", "fc2_experts_weights", "fc2_scales", "slot_out"],
311
  "dispatch": { "x": "groupMaxTiles", "y": "ceilDiv(hiddenSize, groupTileN)" }
312
  },
313
  {
314
  "id": "mix_stage",
315
  "name": "QMoE.MixStage",
316
+ "shader": "expert-slot-mix.wgsl.jinja",
317
+ "bindings": ["slot_out_f32", "route_mix_f32", "output", "params"],
318
  "dispatch": {
319
  "x": "min(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
320
  "y": "ceilDiv(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
 
340
  "id": "route_stage",
341
  "name": "QMoE.RouteStage",
342
  "shader": "qmoe-route-stage.wgsl.jinja",
343
+ "bindings": ["router_probs", "route_expert", "route_mix", "params_route"],
344
  "dispatch": {
345
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
346
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
 
355
  "id": "fc1_activation_stage",
356
  "name": "QMoE.FC1ActivationStageGemv",
357
  "shader": "qmoe-fc1-activation-gemv.wgsl.jinja",
358
+ "bindings": ["input", "route_expert_u32", "fc1_experts_weights", "fc1_scales", "hidden_act", "params__uniform"],
359
  "dispatch": {
360
  "x": "ceilDiv(interSize, decodeRows)",
361
  "y": "min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * topK"
 
366
  "name": "QMoE.OutputStageGemv",
367
  "shader": "qmoe-output-gemv.wgsl.jinja",
368
  "bindings": [
369
+ "hidden_act_f32",
370
+ "route_expert_u32",
371
+ "route_mix_f32",
372
  "fc2_experts_weights",
373
  "fc2_scales",
374
  "output",
 
400
  "id": "route_stage",
401
  "name": "QMoE.RouteStage",
402
  "shader": "qmoe-route-stage.wgsl.jinja",
403
+ "bindings": ["router_probs", "route_expert", "route_mix", "params_route"],
404
  "dispatch": {
405
  "x": "min(ceilDiv((numTokens), (workgroupSize)), 65535)",
406
  "y": "ceilDiv(ceilDiv((numTokens), (workgroupSize)), 65535)",
 
417
  "shader": "qmoe-fc1-activation-stage.wgsl.jinja",
418
  "bindings": [
419
  "input",
420
+ "route_expert_u32",
421
  "fc1_experts_weights",
422
  "fc1_scales",
423
  "hidden_act",
 
445
  "id": "output_stage",
446
  "name": "QMoE.OutputStage",
447
  "shader": "qmoe-output-stage.wgsl.jinja",
448
+ "bindings": ["hidden_act_f32", "route_expert_u32", "route_mix_f32", "fc2_experts_weights", "fc2_scales", "output", "params"],
449
  "dispatch": {
450
  "x": "min(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
451
  "y": "ceilDiv(ceilDiv((min(hiddenChunkTokens, numTokens - repeat.chunk * hiddenChunkTokens) * hiddenSize), (workgroupSize)), 65535)",
build/webgpu/metadata.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "name": "com.microsoft.QMoE",
3
- "id": "_com_microsoft_qmoe_webgpu_b3768d9",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
@@ -9,28 +9,28 @@
9
  "files": {
10
  "bench.json": "YuXSEWa4U+6Ek0E0ZkIeDrzqQ672IelokTilKJuKHf0=",
11
  "expert-group-slots.wgsl.jinja": "Ta+3H2FA1qRRzksdkwgKLZiO+AokmuM+B9JVN8cv1EQ=",
12
- "manifest.json": "r3eaOyzncaumYYrgh5XHHfvd9mHdvVPR3cl5xbiwDDA=",
13
- "qmoe-fc1-activation-gemv.wgsl.jinja": "UvP8Vyx1E9+/+I09w/z9iB6h6MHrSrOmjKfIelyIS0M=",
14
- "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja": "uWNSXPm0htntxHKCNBzDIo5qFmROP9wDF3mjBGQo+Wo=",
15
- "qmoe-fc1-activation-grouped.wgsl.jinja": "6NEYMHvSNgCp6KHJveI4LELrTqr8u/lxIJjSkRX68V4=",
16
- "qmoe-fc1-activation-stage.wgsl.jinja": "C9DvNGQ2u4eBAAceyEdbGX13goQFJbxSunkXXhYN604=",
17
- "qmoe-mix-stage.wgsl.jinja": "+xEc6boXboFbx+4AJ2/KWElM04LRuufmiIKfEK51/oo=",
18
- "qmoe-output-gemv.wgsl.jinja": "OJLoDDWMYE5yaTeDGY7qUeuf6AepJL7ZZLx8ko6OTEQ=",
19
- "qmoe-output-grouped-sgmat.wgsl.jinja": "jopTCpUPoIw9MBRhrmjQuZioTbJJIygwBvbwRyhHvUY=",
20
- "qmoe-output-grouped.wgsl.jinja": "dOoFq8c0+yBCwOae+QVKBQyodJUK4vwam1RQb46oBNU=",
21
- "qmoe-output-stage.wgsl.jinja": "yuSg3G05eKQOunyR5lcCEaJ5y5lXoT73RzT9rq6ahJQ=",
22
  "qmoe-output-zero-inter.wgsl.jinja": "6jHYpVxU5+9WMflhwMste4v17hq7WWA1BDDS4q4DLS4=",
23
- "qmoe-route-stage.wgsl.jinja": "SF5VN6d0zp/ssCGnTwCWccgbnOkZ0mptZiwIbMX3gXA=",
24
- "test.json": "/ffqKBtjk2jseUZPswe8gOyYZfhNJu9aOZg8GNlB+9A="
25
  }
26
  },
27
- "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
28
  "webgpu": {
29
- "manifestSpec": "2.0",
30
  "variants": {
31
  "quant_zero_inter": ["qmoe-output-zero-inter.wgsl.jinja"],
32
- "quant_grouped_sgmat_routed": ["expert-group-slots.wgsl.jinja", "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja", "qmoe-mix-stage.wgsl.jinja", "qmoe-output-grouped-sgmat.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"],
33
- "quant_grouped_routed": ["expert-group-slots.wgsl.jinja", "qmoe-fc1-activation-grouped.wgsl.jinja", "qmoe-mix-stage.wgsl.jinja", "qmoe-output-grouped.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"],
34
  "quant_gemv_routed": ["qmoe-fc1-activation-gemv.wgsl.jinja", "qmoe-output-gemv.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"],
35
  "quant_split_routed": ["qmoe-fc1-activation-stage.wgsl.jinja", "qmoe-output-stage.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"]
36
  }
 
1
  {
2
  "name": "com.microsoft.QMoE",
3
+ "id": "_com_microsoft_qmoe_webgpu_58337b0",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
 
9
  "files": {
10
  "bench.json": "YuXSEWa4U+6Ek0E0ZkIeDrzqQ672IelokTilKJuKHf0=",
11
  "expert-group-slots.wgsl.jinja": "Ta+3H2FA1qRRzksdkwgKLZiO+AokmuM+B9JVN8cv1EQ=",
12
+ "expert-slot-mix.wgsl.jinja": "VwX7A1f2r+lxMzG/I5r0KUpGr0Y5gW/CrI/wdwx0Ah4=",
13
+ "manifest.json": "9eXT25hIM0qWJhCsJjnAodd/OgG9+nO+AvuscEpzk5U=",
14
+ "qmoe-fc1-activation-gemv.wgsl.jinja": "ZYXWDSbc10LldI8RICBHK6Z/9qrrXbuYfLpVnsa0ON4=",
15
+ "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja": "emJWdN2BOxm610l2YstemK7OkegF96pKvhnpRSNQ5LY=",
16
+ "qmoe-fc1-activation-grouped.wgsl.jinja": "TXmpZXCGJhq/T26VJpnp9OcBu14jg4oJCnAvnqY7uF4=",
17
+ "qmoe-fc1-activation-stage.wgsl.jinja": "zpkM+RhDGOwHtlQBDa6k30BgDLJl2MzkCQDH1+fEFGk=",
18
+ "qmoe-output-gemv.wgsl.jinja": "aB5licSktZYGWKyHBnL+K4a7ogTZCG5b0IewORHZTVs=",
19
+ "qmoe-output-grouped-sgmat.wgsl.jinja": "NZocGQlY7dInufxtEwMxylKXDqspt3MyJqUftZ/M6Uk=",
20
+ "qmoe-output-grouped.wgsl.jinja": "P7AAth2FTgq4p67WQpt6rWMdWs5PsAEn0HURW44wIRQ=",
21
+ "qmoe-output-stage.wgsl.jinja": "x1VLEwM3N45620tcpvbRcD94PSrt/gxDUjXsYBDQJAw=",
22
  "qmoe-output-zero-inter.wgsl.jinja": "6jHYpVxU5+9WMflhwMste4v17hq7WWA1BDDS4q4DLS4=",
23
+ "qmoe-route-stage.wgsl.jinja": "wi5Q1NwN1ZugT/otZryovayihC1gP6EdLO9yw1skhtc=",
24
+ "test.json": "6Rt3peYHm7WQraXPO0uYfIdb8VlBXwFwgy9pAvQwiac="
25
  }
26
  },
27
+ "provenance": { "kernel": { "sha": "6fdf6301e2bbcc2f03bf1eaf493b7ad55ef33afc", "dirty": false } },
28
  "webgpu": {
29
+ "manifestSpec": "2.1",
30
  "variants": {
31
  "quant_zero_inter": ["qmoe-output-zero-inter.wgsl.jinja"],
32
+ "quant_grouped_sgmat_routed": ["expert-group-slots.wgsl.jinja", "expert-slot-mix.wgsl.jinja", "qmoe-fc1-activation-grouped-sgmat.wgsl.jinja", "qmoe-output-grouped-sgmat.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"],
33
+ "quant_grouped_routed": ["expert-group-slots.wgsl.jinja", "expert-slot-mix.wgsl.jinja", "qmoe-fc1-activation-grouped.wgsl.jinja", "qmoe-output-grouped.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"],
34
  "quant_gemv_routed": ["qmoe-fc1-activation-gemv.wgsl.jinja", "qmoe-output-gemv.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"],
35
  "quant_split_routed": ["qmoe-fc1-activation-stage.wgsl.jinja", "qmoe-output-stage.wgsl.jinja", "qmoe-route-stage.wgsl.jinja"]
36
  }
build/webgpu/qmoe-fc1-activation-gemv.wgsl.jinja CHANGED
@@ -10,54 +10,57 @@ const TOP_K: u32 = {{ topK }}u;
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 %}
@@ -77,7 +80,6 @@ fn activate(value: f32) -> f32 {
77
  }
78
  {% endif %}
79
 
80
-
81
  {% macro rowlane_fold(arrays, lanes="LANES", lane="lane", slot="slot") %}
82
  var n = {{ lanes }};
83
  while (n > 1u) {
@@ -89,9 +91,7 @@ fn activate(value: f32) -> f32 {
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
 
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
+ {% set qw = "fc1" %}
14
+ {% set qwRows = "FC1_ROWS" %}
15
+ {% set qwPackedCols = fc1PackedCols %}
16
+ {% set qwBlocks = fc1ScaleBlocks %}
17
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
18
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
19
  // first along the reduction axis, two codes per stored word.
20
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
21
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
22
  {% if not colWiseScales %}
23
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
24
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
25
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
26
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
27
  {% endif %}
28
 
29
+ fn {{ qw }}_weight(expert: u32, row: u32, col: u32) -> f32 {
30
+ let base = expert * {{ qwRows }} + row;
31
  {% if packSize == 2 %}
32
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
33
  let code = f32((word >> ((col & 1u) * 4u)) & 15u);
34
  {% else %}
35
+ let code = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u);
36
  {% endif %}
37
  {% if not colWiseScales %}
38
+ return (code - MIDPOINT) * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
39
  {% else %}
40
+ return (code - MIDPOINT) * {{ qw }}_scales[base];
41
  {% endif %}
42
  }
43
  // Decode two consecutive reduction-axis codes together. Four-bit codes share a
44
  // stored word; eight-bit codes occupy adjacent words.
45
+ fn {{ qw }}_weight_pair(expert: u32, row: u32, col: u32) -> vec2<f32> {
46
+ let base = expert * {{ qwRows }} + row;
47
  {% if packSize == 2 %}
48
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
49
  let lo = f32(word & 15u) - MIDPOINT;
50
  let hi = f32((word >> 4u) & 15u) - MIDPOINT;
51
  {% else %}
52
+ let lo = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u) - MIDPOINT;
53
+ let hi = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col + 1u] & 255u) - MIDPOINT;
54
  {% endif %}
55
  {% if not colWiseScales %}
56
+ return vec2<f32>(lo * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK],
57
+ hi * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + (col + 1u) / QUANT_BLOCK]);
58
  {% else %}
59
+ let scale = {{ qw }}_scales[base];
60
  return vec2<f32>(lo * scale, hi * scale);
61
  {% endif %}
62
  }
63
 
 
64
  {% if activationType == "swiglu" %}
65
  fn activate(gate_in: f32, up_in: f32) -> f32 {
66
  {% if hasSwigluLimit %}
 
80
  }
81
  {% endif %}
82
 
 
83
  {% macro rowlane_fold(arrays, lanes="LANES", lane="lane", slot="slot") %}
84
  var n = {{ lanes }};
85
  while (n > 1u) {
 
91
  }
92
  workgroupBarrier();
93
  n = half;
94
+ }{% endmacro %}
 
 
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
build/webgpu/qmoe-fc1-activation-grouped-sgmat.wgsl.jinja CHANGED
@@ -5,7 +5,6 @@ enable subgroup_size_control;
5
  enable chromium_experimental_subgroup_matrix;
6
  diagnostic(off, chromium.subgroup_matrix_uniformity);
7
 
8
-
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  // Expert-grouped FC1 and activation stage using subgroup-matrix operations.
@@ -20,50 +19,53 @@ const HIDDEN: u32 = {{ hidden }}u;
20
  const FC1_ROWS: u32 = {{ fc1Rows }}u;
21
  const INTER: u32 = {{ inter }}u;
22
  const TOP_K: u32 = {{ topK }}u;
23
- const TILE_COLS: u32 = 64u;
24
  const TILE_K: u32 = 32u;
25
  const SUB_COLS: u32 = 32u;
26
  const SUB_ROWS: u32 = 16u;
27
  {% set isSwiglu = activationType == "swiglu" %}
28
  // Weight rows per output column: the fused gate/up pair, or one.
29
  const FUSION: u32 = {{ 2 if isSwiglu else 1 }}u;
 
 
 
 
30
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
31
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
32
  // first along the reduction axis, two codes per stored word.
33
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
34
- const FC1_PACKED_COLS: u32 = {{ fc1PackedCols }}u;
35
  {% if not colWiseScales %}
36
- // Block-wise scales (fc1_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
37
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
38
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
39
- const FC1_BLOCKS: u32 = {{ fc1ScaleBlocks }}u;
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
  // this helper requires `col` to be a multiple of four.
45
- fn fc1_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
46
- let base = expert * FC1_ROWS + row;
47
  {% if packSize == 2 %}
48
- let pair = base * FC1_PACKED_COLS + col / 2u;
49
- let w0 = fc1_experts_weights[pair];
50
- let w1 = fc1_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 * FC1_PACKED_COLS + col;
55
- let codes = vec4<f32>(f32(fc1_experts_weights[word] & 255u), f32(fc1_experts_weights[word + 1u] & 255u),
56
- f32(fc1_experts_weights[word + 2u] & 255u), f32(fc1_experts_weights[word + 3u] & 255u));
57
  {% endif %}
58
  {% if not colWiseScales %}
59
- let scale = fc1_scales[base * FC1_BLOCKS + col / QUANT_BLOCK];
60
  {% else %}
61
- let scale = fc1_scales[base];
62
  {% endif %}
63
  return (codes - vec4<f32>(MIDPOINT)) * scale;
64
  }
65
 
66
-
67
  {% if activationType == "swiglu" %}
68
  fn activate(gate_in: f32, up_in: f32) -> f32 {
69
  {% if hasSwigluLimit %}
@@ -83,11 +85,10 @@ fn activate(value: f32) -> f32 {
83
  }
84
  {% endif %}
85
 
86
-
87
  // The final K-loop barrier ends the A operand's lifetime. Its storage then
88
  // holds four 8x8 output banks per subgroup, fenced by the epilogue barriers.
89
  var<workgroup> tile_A: array<f32, {{ groupedSgmatStageElements }}>;
90
- var<workgroup> tile_B: array<f32, 64 * 32>;
91
 
92
  // Rows past the tile's end stage zero and are dropped at the store, so a short final
93
  // tile needs no separate arm.
@@ -149,9 +150,11 @@ fn main(
149
  let base_A = subtile_idy * SUB_ROWS;
150
  let base_B = subtile_idx * SUB_COLS;
151
 
152
- {% for row in [0, 1] %}{% for col in [0, 1, 2, 3] %}
 
153
  var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, 8, 8>;
154
- {% endfor %}{% endfor %}
 
155
 
156
  for (var kidx = 0u; kidx < HIDDEN; kidx = kidx + TILE_K) {
157
  loadSHMA(kidx, rows, local_idx / 4u, local_idx % 4u, a_slot);
@@ -159,7 +162,8 @@ fn main(
159
  workgroupBarrier();
160
 
161
  for (var step = 0u; step < TILE_K; step = step + 8u) {
162
- {% set operandScalar = "f32" %}{% set directInputs = directMatrixInputs is defined and directMatrixInputs %}
 
163
  let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
164
  {% for r in range(2) %}
165
  var matA{{ r }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ "w" if directInputs else "tile_A" }}, matrix_a_offset{% if r > 0 %} + 8u * {{ "K" if directInputs else "TILE_K" }}{% endif %}, {{ "K" if directInputs else "TILE_K" }});
 
5
  enable chromium_experimental_subgroup_matrix;
6
  diagnostic(off, chromium.subgroup_matrix_uniformity);
7
 
 
8
  {{ env.wgsl.resourceDeclarations }}
9
 
10
  // Expert-grouped FC1 and activation stage using subgroup-matrix operations.
 
19
  const FC1_ROWS: u32 = {{ fc1Rows }}u;
20
  const INTER: u32 = {{ inter }}u;
21
  const TOP_K: u32 = {{ topK }}u;
22
+ const TILE_COLS: u32 = {{ sgmatTileCols }}u;
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
+ {% set qw = "fc1" %}
30
+ {% set qwRows = "FC1_ROWS" %}
31
+ {% set qwPackedCols = fc1PackedCols %}
32
+ {% set qwBlocks = fc1ScaleBlocks %}
33
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
34
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
35
  // first along the reduction axis, two codes per stored word.
36
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
37
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
38
  {% if not colWiseScales %}
39
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
40
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
41
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
42
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
43
  {% endif %}
44
 
45
  // Decode the four-code group consumed by the staging loop. The aligned codes
46
  // share one quantization scale: blocks are powers of two of at least 16, and
47
  // this helper requires `col` to be a multiple of four.
48
+ fn {{ qw }}_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
49
+ let base = expert * {{ qwRows }} + row;
50
  {% if packSize == 2 %}
51
+ let pair = base * {{ qw | upper }}_PACKED_COLS + col / 2u;
52
+ let w0 = {{ qw }}_experts_weights[pair];
53
+ let w1 = {{ qw }}_experts_weights[pair + 1u];
54
  let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
55
  f32(w1 & 15u), f32((w1 >> 4u) & 15u));
56
  {% else %}
57
+ let word = base * {{ qw | upper }}_PACKED_COLS + col;
58
+ let codes = vec4<f32>(f32({{ qw }}_experts_weights[word] & 255u), f32({{ qw }}_experts_weights[word + 1u] & 255u),
59
+ f32({{ qw }}_experts_weights[word + 2u] & 255u), f32({{ qw }}_experts_weights[word + 3u] & 255u));
60
  {% endif %}
61
  {% if not colWiseScales %}
62
+ let scale = {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
63
  {% else %}
64
+ let scale = {{ qw }}_scales[base];
65
  {% endif %}
66
  return (codes - vec4<f32>(MIDPOINT)) * scale;
67
  }
68
 
 
69
  {% if activationType == "swiglu" %}
70
  fn activate(gate_in: f32, up_in: f32) -> f32 {
71
  {% if hasSwigluLimit %}
 
85
  }
86
  {% endif %}
87
 
 
88
  // The final K-loop barrier ends the A operand's lifetime. Its storage then
89
  // holds four 8x8 output banks per subgroup, fenced by the epilogue barriers.
90
  var<workgroup> tile_A: array<f32, {{ groupedSgmatStageElements }}>;
91
+ var<workgroup> tile_B: array<f32, {{ sgmatTileCols }} * 32>;
92
 
93
  // Rows past the tile's end stage zero and are dropped at the store, so a short final
94
  // tile needs no separate arm.
 
150
  let base_A = subtile_idy * SUB_ROWS;
151
  let base_B = subtile_idx * SUB_COLS;
152
 
153
+ {% for row in [0, 1] %}
154
+ {% for col in [0, 1, 2, 3] %}
155
  var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, 8, 8>;
156
+ {% endfor %}
157
+ {% endfor %}
158
 
159
  for (var kidx = 0u; kidx < HIDDEN; kidx = kidx + TILE_K) {
160
  loadSHMA(kidx, rows, local_idx / 4u, local_idx % 4u, a_slot);
 
162
  workgroupBarrier();
163
 
164
  for (var step = 0u; step < TILE_K; step = step + 8u) {
165
+ {% set operandScalar = "f32" %}
166
+ {% set directInputs = false %}
167
  let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
168
  {% for r in range(2) %}
169
  var matA{{ r }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ "w" if directInputs else "tile_A" }}, matrix_a_offset{% if r > 0 %} + 8u * {{ "K" if directInputs else "TILE_K" }}{% endif %}, {{ "K" if directInputs else "TILE_K" }});
build/webgpu/qmoe-fc1-activation-grouped.wgsl.jinja CHANGED
@@ -15,57 +15,60 @@ 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
  // this helper requires `col` to be 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 %}
@@ -85,7 +88,6 @@ fn activate(value: f32) -> f32 {
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) {
@@ -138,8 +140,7 @@ const KVEC: u32 = {{ groupTileKVec }}u;
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) %}
@@ -192,9 +193,7 @@ const KVEC: u32 = {{ groupTileKVec }}u;
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 }}>;
 
15
  const THREADS: u32 = THREADS_SIDE * THREADS_SIDE;
16
  {% set isSwiglu = activationType == "swiglu" %}
17
  {% set primaryRow = "col * 2u" if isSwiglu else "col" %}
18
+ {% set qw = "fc1" %}
19
+ {% set qwRows = "FC1_ROWS" %}
20
+ {% set qwPackedCols = fc1PackedCols %}
21
+ {% set qwBlocks = fc1ScaleBlocks %}
22
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
23
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
24
  // first along the reduction axis, two codes per stored word.
25
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
26
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
27
  {% if not colWiseScales %}
28
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
29
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
30
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
31
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
32
  {% endif %}
33
 
34
+ fn {{ qw }}_weight(expert: u32, row: u32, col: u32) -> f32 {
35
+ let base = expert * {{ qwRows }} + row;
36
  {% if packSize == 2 %}
37
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
38
  let code = f32((word >> ((col & 1u) * 4u)) & 15u);
39
  {% else %}
40
+ let code = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u);
41
  {% endif %}
42
  {% if not colWiseScales %}
43
+ return (code - MIDPOINT) * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
44
  {% else %}
45
+ return (code - MIDPOINT) * {{ qw }}_scales[base];
46
  {% endif %}
47
  }
48
  // Decode the four-code group consumed by the staging loop. The aligned codes
49
  // share one quantization scale: blocks are powers of two of at least 16, and
50
  // this helper requires `col` to be a multiple of four.
51
+ fn {{ qw }}_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
52
+ let base = expert * {{ qwRows }} + row;
53
  {% if packSize == 2 %}
54
+ let pair = base * {{ qw | upper }}_PACKED_COLS + col / 2u;
55
+ let w0 = {{ qw }}_experts_weights[pair];
56
+ let w1 = {{ qw }}_experts_weights[pair + 1u];
57
  let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
58
  f32(w1 & 15u), f32((w1 >> 4u) & 15u));
59
  {% else %}
60
+ let word = base * {{ qw | upper }}_PACKED_COLS + col;
61
+ let codes = vec4<f32>(f32({{ qw }}_experts_weights[word] & 255u), f32({{ qw }}_experts_weights[word + 1u] & 255u),
62
+ f32({{ qw }}_experts_weights[word + 2u] & 255u), f32({{ qw }}_experts_weights[word + 3u] & 255u));
63
  {% endif %}
64
  {% if not colWiseScales %}
65
+ let scale = {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
66
  {% else %}
67
+ let scale = {{ qw }}_scales[base];
68
  {% endif %}
69
  return (codes - vec4<f32>(MIDPOINT)) * scale;
70
  }
71
 
 
72
  {% if activationType == "swiglu" %}
73
  fn activate(gate_in: f32, up_in: f32) -> f32 {
74
  {% if hasSwigluLimit %}
 
88
  }
89
  {% endif %}
90
 
 
91
  const KVEC: u32 = {{ groupTileKVec }}u;
92
  {% macro stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, guarded, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
93
  for (var idx = tid; idx < MTILE * KVEC; idx = idx + THREADS) {
 
140
  {% if b2Load %}
141
  b2_tile[idx] = b2_vec;
142
  {% endif %}
143
+ }{% endmacro %}
 
144
 
145
  {% macro group_tile_loop(aLoad, bLoad, b2Load, kExtent, nExtent, regM, regN, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
146
  {% for r in range(regM) %}
 
193
  // Orders this step's tile reads before the next step overwrites them.
194
  workgroupBarrier();
195
  k_base = k_base + KTILE;
196
+ }{% endmacro %}
 
 
197
  var<workgroup> row_slot: array<u32, {{ groupTileM }}>;
198
  var<workgroup> a_tile: array<vec4<f32>, {{ groupTileM * groupTileKVec }}>;
199
  var<workgroup> b_tile: array<vec4<f32>, {{ groupTileN * groupTileKVec }}>;
build/webgpu/qmoe-fc1-activation-stage.wgsl.jinja CHANGED
@@ -1,4 +1,31 @@
 
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  const HIDDEN: u32 = {{ hidden }}u;
4
  const FC1_ROWS: u32 = {{ fc1Rows }}u;
@@ -11,34 +38,37 @@ 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 %}
@@ -58,12 +88,9 @@ fn activate(value: f32) -> f32 {
58
  }
59
  {% endif %}
60
 
61
-
62
  @compute @workgroup_size(WG, 1, 1)
63
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
64
- // 2D-folded flat index: gid.y carries the high bits past the per-axis dispatch fold width.
65
- // Reduces to gid.x when the dispatch does not fold.
66
- let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
67
  {% if topK > 1 %}
68
  // One activation row per routed slot rather than per token: the grid is
69
  // (token, slot, inter) and hidden_act is addressed by the same flat index.
@@ -91,25 +118,25 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
91
  let expert = route_expert[token];
92
  {% endif %}
93
 
94
- var gate = 0.0;
95
  {% if activationType == "swiglu" %}
96
- var up = 0.0;
97
  {% endif %}
98
  for (var h: u32 = 0u; h < HIDDEN; h = h + 1u) {
99
  let x = f32(input[token * HIDDEN + h]);
100
  {% if activationType == "swiglu" %}
101
- gate = gate + x * fc1_weight(expert, i * 2u, h);
102
  {% else %}
103
- gate = gate + x * fc1_weight(expert, i, h);
104
  {% endif %}
105
  {% if activationType == "swiglu" %}
106
- up = up + x * fc1_weight(expert, i * 2u + 1u, h);
107
  {% endif %}
108
  }
109
 
110
  {% if activationType == "swiglu" %}
111
- hidden_act[index] = activate(gate, up);
112
  {% else %}
113
- hidden_act[index] = activate(gate);
114
  {% endif %}
115
  }
 
1
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
2
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width.
5
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};{% endmacro %}
6
  {{ env.wgsl.resourceDeclarations }}
7
+ {% set dotType = "f32" %}
8
+ // Retain product and addition residuals across a dot product. Explicit fma
9
+ // boundaries preserve the addition error transform under reassociation.
10
+ struct DotAccumulator {
11
+ hi: {{ dotType }},
12
+ lo: {{ dotType }},
13
+ }
14
+ fn dot_accumulate(acc: DotAccumulator, a: {{ dotType }}, b: {{ dotType }}) -> DotAccumulator {
15
+ let product = fma(a, b, {{ dotType }}(0.0));
16
+ let productError = fma(a, b, -product);
17
+ let sum = fma(acc.hi, {{ dotType }}(1.0), product);
18
+ let bv = fma({{ dotType }}(-1.0), acc.hi, sum);
19
+ let av = fma({{ dotType }}(-1.0), bv, sum);
20
+ let ae = fma({{ dotType }}(-1.0), av, acc.hi);
21
+ let be = fma({{ dotType }}(-1.0), bv, product);
22
+ let error = fma(fma(fma(ae, {{ dotType }}(1.0), be), {{ dotType }}(1.0), acc.lo), {{ dotType }}(1.0), productError);
23
+ let hi = fma(sum, {{ dotType }}(1.0), error);
24
+ return DotAccumulator(hi, fma({{ dotType }}(-1.0), fma({{ dotType }}(-1.0), sum, hi), error));
25
+ }
26
+ fn dot_value(acc: DotAccumulator) -> {{ dotType }} {
27
+ return fma(acc.hi, {{ dotType }}(1.0), acc.lo);
28
+ }
29
 
30
  const HIDDEN: u32 = {{ hidden }}u;
31
  const FC1_ROWS: u32 = {{ fc1Rows }}u;
 
38
  {% if topK > 1 %}
39
  const TOP_K: u32 = {{ topK }}u;
40
  {% endif %}
41
+ {% set qw = "fc1" %}
42
+ {% set qwRows = "FC1_ROWS" %}
43
+ {% set qwPackedCols = fc1PackedCols %}
44
+ {% set qwBlocks = fc1ScaleBlocks %}
45
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
46
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
47
  // first along the reduction axis, two codes per stored word.
48
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
49
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
50
  {% if not colWiseScales %}
51
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
52
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
53
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
54
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
55
  {% endif %}
56
 
57
+ fn {{ qw }}_weight(expert: u32, row: u32, col: u32) -> f32 {
58
+ let base = expert * {{ qwRows }} + row;
59
  {% if packSize == 2 %}
60
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
61
  let code = f32((word >> ((col & 1u) * 4u)) & 15u);
62
  {% else %}
63
+ let code = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u);
64
  {% endif %}
65
  {% if not colWiseScales %}
66
+ return (code - MIDPOINT) * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
67
  {% else %}
68
+ return (code - MIDPOINT) * {{ qw }}_scales[base];
69
  {% endif %}
70
  }
71
 
 
72
  {% if activationType == "swiglu" %}
73
  fn activate(gate_in: f32, up_in: f32) -> f32 {
74
  {% if hasSwigluLimit %}
 
88
  }
89
  {% endif %}
90
 
 
91
  @compute @workgroup_size(WG, 1, 1)
92
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
93
+ {{ flat_index_2d("WG", "index", "") }}
 
 
94
  {% if topK > 1 %}
95
  // One activation row per routed slot rather than per token: the grid is
96
  // (token, slot, inter) and hidden_act is addressed by the same flat index.
 
118
  let expert = route_expert[token];
119
  {% endif %}
120
 
121
+ var gate = DotAccumulator(0.0, 0.0);
122
  {% if activationType == "swiglu" %}
123
+ var up = DotAccumulator(0.0, 0.0);
124
  {% endif %}
125
  for (var h: u32 = 0u; h < HIDDEN; h = h + 1u) {
126
  let x = f32(input[token * HIDDEN + h]);
127
  {% if activationType == "swiglu" %}
128
+ gate = dot_accumulate(gate, x, fc1_weight(expert, i * 2u, h));
129
  {% else %}
130
+ gate = dot_accumulate(gate, x, fc1_weight(expert, i, h));
131
  {% endif %}
132
  {% if activationType == "swiglu" %}
133
+ up = dot_accumulate(up, x, fc1_weight(expert, i * 2u + 1u, h));
134
  {% endif %}
135
  }
136
 
137
  {% if activationType == "swiglu" %}
138
+ hidden_act[index] = activate(dot_value(gate), dot_value(up));
139
  {% else %}
140
+ hidden_act[index] = activate(dot_value(gate));
141
  {% endif %}
142
  }
build/webgpu/qmoe-output-gemv.wgsl.jinja CHANGED
@@ -8,54 +8,57 @@ const TOP_K: u32 = {{ topK }}u;
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) {
@@ -67,9 +70,7 @@ fn fc2_weight_pair(expert: u32, row: u32, col: u32) -> vec2<f32> {
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)
 
8
  // LANES-wide cooperative reduction.
9
  const LANES: u32 = {{ decodeLanes }}u;
10
  const ROWS: u32 = {{ decodeRows }}u;
11
+ {% set qw = "fc2" %}
12
+ {% set qwRows = "HIDDEN" %}
13
+ {% set qwPackedCols = fc2PackedCols %}
14
+ {% set qwBlocks = fc2ScaleBlocks %}
15
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
16
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
17
  // first along the reduction axis, two codes per stored word.
18
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
19
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
20
  {% if not colWiseScales %}
21
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
22
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
23
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
24
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
25
  {% endif %}
26
 
27
+ fn {{ qw }}_weight(expert: u32, row: u32, col: u32) -> f32 {
28
+ let base = expert * {{ qwRows }} + row;
29
  {% if packSize == 2 %}
30
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
31
  let code = f32((word >> ((col & 1u) * 4u)) & 15u);
32
  {% else %}
33
+ let code = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u);
34
  {% endif %}
35
  {% if not colWiseScales %}
36
+ return (code - MIDPOINT) * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
37
  {% else %}
38
+ return (code - MIDPOINT) * {{ qw }}_scales[base];
39
  {% endif %}
40
  }
41
  // Decode two consecutive reduction-axis codes together. Four-bit codes share a
42
  // stored word; eight-bit codes occupy adjacent words.
43
+ fn {{ qw }}_weight_pair(expert: u32, row: u32, col: u32) -> vec2<f32> {
44
+ let base = expert * {{ qwRows }} + row;
45
  {% if packSize == 2 %}
46
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
47
  let lo = f32(word & 15u) - MIDPOINT;
48
  let hi = f32((word >> 4u) & 15u) - MIDPOINT;
49
  {% else %}
50
+ let lo = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u) - MIDPOINT;
51
+ let hi = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col + 1u] & 255u) - MIDPOINT;
52
  {% endif %}
53
  {% if not colWiseScales %}
54
+ return vec2<f32>(lo * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK],
55
+ hi * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + (col + 1u) / QUANT_BLOCK]);
56
  {% else %}
57
+ let scale = {{ qw }}_scales[base];
58
  return vec2<f32>(lo * scale, hi * scale);
59
  {% endif %}
60
  }
61
 
 
62
  {% macro rowlane_fold(arrays, lanes="LANES", lane="lane", slot="slot") %}
63
  var n = {{ lanes }};
64
  while (n > 1u) {
 
70
  }
71
  workgroupBarrier();
72
  n = half;
73
+ }{% endmacro %}
 
 
74
  var<workgroup> partial: array<f32, {{ decodeLanes * decodeRows }}>;
75
 
76
  @compute @workgroup_size(LANES, ROWS, 1)
build/webgpu/qmoe-output-grouped-sgmat.wgsl.jinja CHANGED
@@ -5,7 +5,6 @@ enable subgroup_size_control;
5
  enable chromium_experimental_subgroup_matrix;
6
  diagnostic(off, chromium.subgroup_matrix_uniformity);
7
 
8
-
9
  {{ env.wgsl.resourceDeclarations }}
10
 
11
  // Expert-grouped FC2 stage using subgroup-matrix operations. Routed slots are
@@ -17,51 +16,54 @@ diagnostic(off, chromium.subgroup_matrix_uniformity);
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_COLS: u32 = 64u;
21
  const TILE_K: u32 = 32u;
22
  const SUB_COLS: u32 = 32u;
23
  const SUB_ROWS: u32 = 16u;
 
 
 
 
24
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
25
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
26
  // first along the reduction axis, two codes per stored word.
27
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
28
- const FC2_PACKED_COLS: u32 = {{ fc2PackedCols }}u;
29
  {% if not colWiseScales %}
30
- // Block-wise scales (fc2_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
31
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
32
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
33
- const FC2_BLOCKS: u32 = {{ fc2ScaleBlocks }}u;
34
  {% endif %}
35
 
36
  // Decode the four-code group consumed by the staging loop. The aligned codes
37
  // share one quantization scale: blocks are powers of two of at least 16, and
38
  // this helper requires `col` to be a multiple of four.
39
- fn fc2_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
40
- let base = expert * HIDDEN + row;
41
  {% if packSize == 2 %}
42
- let pair = base * FC2_PACKED_COLS + col / 2u;
43
- let w0 = fc2_experts_weights[pair];
44
- let w1 = fc2_experts_weights[pair + 1u];
45
  let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
46
  f32(w1 & 15u), f32((w1 >> 4u) & 15u));
47
  {% else %}
48
- let word = base * FC2_PACKED_COLS + col;
49
- let codes = vec4<f32>(f32(fc2_experts_weights[word] & 255u), f32(fc2_experts_weights[word + 1u] & 255u),
50
- f32(fc2_experts_weights[word + 2u] & 255u), f32(fc2_experts_weights[word + 3u] & 255u));
51
  {% endif %}
52
  {% if not colWiseScales %}
53
- let scale = fc2_scales[base * FC2_BLOCKS + col / QUANT_BLOCK];
54
  {% else %}
55
- let scale = fc2_scales[base];
56
  {% endif %}
57
  return (codes - vec4<f32>(MIDPOINT)) * scale;
58
  }
59
 
60
-
61
  // The final K-loop barrier ends the A operand's lifetime. Its storage then
62
  // holds four 8x8 output banks per subgroup, fenced by the epilogue barriers.
63
  var<workgroup> tile_A: array<f32, {{ groupedSgmatStageElements }}>;
64
- var<workgroup> tile_B: array<f32, 64 * 32>;
65
 
66
  // Rows past the tile's end stage zero and are dropped at the store, so a short final
67
  // tile needs no separate arm.
@@ -141,9 +143,11 @@ fn main(
141
  let base_A = subtile_idy * SUB_ROWS;
142
  let base_B = subtile_idx * SUB_COLS;
143
 
144
- {% for row in [0, 1] %}{% for col in [0, 1, 2, 3] %}
 
145
  var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, 8, 8>;
146
- {% endfor %}{% endfor %}
 
147
 
148
  for (var kidx = 0u; kidx < INTER; kidx = kidx + TILE_K) {
149
  loadSHMA(kidx, rows, local_idx / 4u, local_idx % 4u, a_slot);
@@ -151,7 +155,8 @@ fn main(
151
  workgroupBarrier();
152
 
153
  for (var step = 0u; step < TILE_K; step = step + 8u) {
154
- {% set operandScalar = "f32" %}{% set directInputs = directMatrixInputs is defined and directMatrixInputs %}
 
155
  let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
156
  {% for r in range(2) %}
157
  var matA{{ r }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ "w" if directInputs else "tile_A" }}, matrix_a_offset{% if r > 0 %} + 8u * {{ "K" if directInputs else "TILE_K" }}{% endif %}, {{ "K" if directInputs else "TILE_K" }});
 
5
  enable chromium_experimental_subgroup_matrix;
6
  diagnostic(off, chromium.subgroup_matrix_uniformity);
7
 
 
8
  {{ env.wgsl.resourceDeclarations }}
9
 
10
  // Expert-grouped FC2 stage using subgroup-matrix operations. Routed slots are
 
16
  // A loader gathers through `slot_list` and the store scatters back through it.
17
  const HIDDEN: u32 = {{ hidden }}u;
18
  const INTER: u32 = {{ inter }}u;
19
+ const TILE_COLS: u32 = {{ sgmatTileCols }}u;
20
  const TILE_K: u32 = 32u;
21
  const SUB_COLS: u32 = 32u;
22
  const SUB_ROWS: u32 = 16u;
23
+ {% set qw = "fc2" %}
24
+ {% set qwRows = "HIDDEN" %}
25
+ {% set qwPackedCols = fc2PackedCols %}
26
+ {% set qwBlocks = fc2ScaleBlocks %}
27
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
28
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
29
  // first along the reduction axis, two codes per stored word.
30
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
31
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
32
  {% if not colWiseScales %}
33
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
34
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
35
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
36
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
37
  {% endif %}
38
 
39
  // Decode the four-code group consumed by the staging loop. The aligned codes
40
  // share one quantization scale: blocks are powers of two of at least 16, and
41
  // this helper requires `col` to be a multiple of four.
42
+ fn {{ qw }}_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
43
+ let base = expert * {{ qwRows }} + row;
44
  {% if packSize == 2 %}
45
+ let pair = base * {{ qw | upper }}_PACKED_COLS + col / 2u;
46
+ let w0 = {{ qw }}_experts_weights[pair];
47
+ let w1 = {{ qw }}_experts_weights[pair + 1u];
48
  let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
49
  f32(w1 & 15u), f32((w1 >> 4u) & 15u));
50
  {% else %}
51
+ let word = base * {{ qw | upper }}_PACKED_COLS + col;
52
+ let codes = vec4<f32>(f32({{ qw }}_experts_weights[word] & 255u), f32({{ qw }}_experts_weights[word + 1u] & 255u),
53
+ f32({{ qw }}_experts_weights[word + 2u] & 255u), f32({{ qw }}_experts_weights[word + 3u] & 255u));
54
  {% endif %}
55
  {% if not colWiseScales %}
56
+ let scale = {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
57
  {% else %}
58
+ let scale = {{ qw }}_scales[base];
59
  {% endif %}
60
  return (codes - vec4<f32>(MIDPOINT)) * scale;
61
  }
62
 
 
63
  // The final K-loop barrier ends the A operand's lifetime. Its storage then
64
  // holds four 8x8 output banks per subgroup, fenced by the epilogue barriers.
65
  var<workgroup> tile_A: array<f32, {{ groupedSgmatStageElements }}>;
66
+ var<workgroup> tile_B: array<f32, {{ sgmatTileCols }} * 32>;
67
 
68
  // Rows past the tile's end stage zero and are dropped at the store, so a short final
69
  // tile needs no separate arm.
 
143
  let base_A = subtile_idy * SUB_ROWS;
144
  let base_B = subtile_idx * SUB_COLS;
145
 
146
+ {% for row in [0, 1] %}
147
+ {% for col in [0, 1, 2, 3] %}
148
  var matC{{ row }}{{ col }}: subgroup_matrix_result<f32, 8, 8>;
149
+ {% endfor %}
150
+ {% endfor %}
151
 
152
  for (var kidx = 0u; kidx < INTER; kidx = kidx + TILE_K) {
153
  loadSHMA(kidx, rows, local_idx / 4u, local_idx % 4u, a_slot);
 
155
  workgroupBarrier();
156
 
157
  for (var step = 0u; step < TILE_K; step = step + 8u) {
158
+ {% set operandScalar = "f32" %}
159
+ {% set directInputs = false %}
160
  let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
161
  {% for r in range(2) %}
162
  var matA{{ r }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ "w" if directInputs else "tile_A" }}, matrix_a_offset{% if r > 0 %} + 8u * {{ "K" if directInputs else "TILE_K" }}{% endif %}, {{ "K" if directInputs else "TILE_K" }});
build/webgpu/qmoe-output-grouped.wgsl.jinja CHANGED
@@ -13,57 +13,60 @@ 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
  // this helper requires `col` to be 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) {
@@ -116,16 +119,12 @@ const KVEC: u32 = {{ groupTileKVec }}u;
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
 
@@ -154,25 +153,17 @@ const KVEC: u32 = {{ groupTileKVec }}u;
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 }}>;
 
13
  const KTILE: u32 = {{ groupTileK }}u;
14
  const THREADS_SIDE: u32 = {{ groupThreads }}u;
15
  const THREADS: u32 = THREADS_SIDE * THREADS_SIDE;
16
+ {% set qw = "fc2" %}
17
+ {% set qwRows = "HIDDEN" %}
18
+ {% set qwPackedCols = fc2PackedCols %}
19
+ {% set qwBlocks = fc2ScaleBlocks %}
20
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
21
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
22
  // first along the reduction axis, two codes per stored word.
23
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
24
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
25
  {% if not colWiseScales %}
26
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
27
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
28
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
29
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
30
  {% endif %}
31
 
32
+ fn {{ qw }}_weight(expert: u32, row: u32, col: u32) -> f32 {
33
+ let base = expert * {{ qwRows }} + row;
34
  {% if packSize == 2 %}
35
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
36
  let code = f32((word >> ((col & 1u) * 4u)) & 15u);
37
  {% else %}
38
+ let code = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u);
39
  {% endif %}
40
  {% if not colWiseScales %}
41
+ return (code - MIDPOINT) * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
42
  {% else %}
43
+ return (code - MIDPOINT) * {{ qw }}_scales[base];
44
  {% endif %}
45
  }
46
  // Decode the four-code group consumed by the staging loop. The aligned codes
47
  // share one quantization scale: blocks are powers of two of at least 16, and
48
  // this helper requires `col` to be a multiple of four.
49
+ fn {{ qw }}_weight_vec4(expert: u32, row: u32, col: u32) -> vec4<f32> {
50
+ let base = expert * {{ qwRows }} + row;
51
  {% if packSize == 2 %}
52
+ let pair = base * {{ qw | upper }}_PACKED_COLS + col / 2u;
53
+ let w0 = {{ qw }}_experts_weights[pair];
54
+ let w1 = {{ qw }}_experts_weights[pair + 1u];
55
  let codes = vec4<f32>(f32(w0 & 15u), f32((w0 >> 4u) & 15u),
56
  f32(w1 & 15u), f32((w1 >> 4u) & 15u));
57
  {% else %}
58
+ let word = base * {{ qw | upper }}_PACKED_COLS + col;
59
+ let codes = vec4<f32>(f32({{ qw }}_experts_weights[word] & 255u), f32({{ qw }}_experts_weights[word + 1u] & 255u),
60
+ f32({{ qw }}_experts_weights[word + 2u] & 255u), f32({{ qw }}_experts_weights[word + 3u] & 255u));
61
  {% endif %}
62
  {% if not colWiseScales %}
63
+ let scale = {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
64
  {% else %}
65
+ let scale = {{ qw }}_scales[base];
66
  {% endif %}
67
  return (codes - vec4<f32>(MIDPOINT)) * scale;
68
  }
69
 
 
70
  const KVEC: u32 = {{ groupTileKVec }}u;
71
  {% macro stage_group_tiles(aLoad, bLoad, b2Load, kExtent, nExtent, guarded, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
72
  for (var idx = tid; idx < MTILE * KVEC; idx = idx + THREADS) {
 
119
  {% if b2Load %}
120
  b2_tile[idx] = b2_vec;
121
  {% endif %}
122
+ }{% endmacro %}
 
123
 
124
  {% macro group_tile_loop(aLoad, bLoad, b2Load, kExtent, nExtent, regM, regN, bLoadVec4="", b2LoadVec4="", aLoadVec4="") %}
125
  {% for r in range(regM) %}
126
  {% for c in range(regN) %}
127
  var acc_{{ r }}_{{ c }} = 0.0;
 
 
 
128
  {% endfor %}
129
  {% endfor %}
130
 
 
153
  {% endfor %}
154
  {% for c in range(regN) %}
155
  let b{{ c }} = b_tile[(lid.x * {{ regN }}u + {{ c }}u) * KVEC + kv];
 
 
 
156
  {% endfor %}
157
  {% for r in range(regM) %}
158
  {% for c in range(regN) %}
159
  acc_{{ r }}_{{ c }} = acc_{{ r }}_{{ c }} + dot(a{{ r }}, b{{ c }});
 
 
 
160
  {% endfor %}
161
  {% endfor %}
162
  }
163
  // Orders this step's tile reads before the next step overwrites them.
164
  workgroupBarrier();
165
  k_base = k_base + KTILE;
166
+ }{% endmacro %}
 
 
167
  var<workgroup> row_slot: array<u32, {{ groupTileM }}>;
168
  var<workgroup> a_tile: array<vec4<f32>, {{ groupTileM * groupTileKVec }}>;
169
  var<workgroup> b_tile: array<vec4<f32>, {{ groupTileN * groupTileKVec }}>;
build/webgpu/qmoe-output-stage.wgsl.jinja CHANGED
@@ -1,4 +1,31 @@
 
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  const HIDDEN: u32 = {{ hidden }}u;
4
  const INTER: u32 = {{ inter }}u;
@@ -6,39 +33,40 @@ 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
- // 2D-folded flat index: gid.y carries the high bits past the per-axis dispatch fold width.
40
- // Reduces to gid.x when the dispatch does not fold.
41
- let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
42
  let total = params.tokenCount * HIDDEN;
43
  if (index >= total) {
44
  return;
@@ -55,11 +83,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
55
  var acc = 0.0;
56
  for (var j = 0u; j < TOP_K; j = j + 1u) {
57
  let expert = route_expert[token * TOP_K + j];
58
- var part = 0.0;
59
  for (var i: u32 = 0u; i < INTER; i = i + 1u) {
60
- part = part + hidden_act[(local_token * TOP_K + j) * INTER + i] * fc2_weight(expert, out_col, i);
61
  }
62
- acc = acc + part * route_mix[token * TOP_K + j];
63
  }
64
 
65
  output[token * HIDDEN + out_col] = acc;
@@ -67,11 +95,11 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
67
  let expert = route_expert[token];
68
  let mix = route_mix[token];
69
 
70
- var acc = 0.0;
71
  for (var i: u32 = 0u; i < INTER; i = i + 1u) {
72
- acc = acc + hidden_act[local_token * INTER + i] * fc2_weight(expert, out_col, i);
73
  }
74
 
75
- output[token * HIDDEN + out_col] = acc * mix;
76
  {% endif %}
77
  }
 
1
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
2
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width.
5
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};{% endmacro %}
6
  {{ env.wgsl.resourceDeclarations }}
7
+ {% set dotType = "f32" %}
8
+ // Retain product and addition residuals across a dot product. Explicit fma
9
+ // boundaries preserve the addition error transform under reassociation.
10
+ struct DotAccumulator {
11
+ hi: {{ dotType }},
12
+ lo: {{ dotType }},
13
+ }
14
+ fn dot_accumulate(acc: DotAccumulator, a: {{ dotType }}, b: {{ dotType }}) -> DotAccumulator {
15
+ let product = fma(a, b, {{ dotType }}(0.0));
16
+ let productError = fma(a, b, -product);
17
+ let sum = fma(acc.hi, {{ dotType }}(1.0), product);
18
+ let bv = fma({{ dotType }}(-1.0), acc.hi, sum);
19
+ let av = fma({{ dotType }}(-1.0), bv, sum);
20
+ let ae = fma({{ dotType }}(-1.0), av, acc.hi);
21
+ let be = fma({{ dotType }}(-1.0), bv, product);
22
+ let error = fma(fma(fma(ae, {{ dotType }}(1.0), be), {{ dotType }}(1.0), acc.lo), {{ dotType }}(1.0), productError);
23
+ let hi = fma(sum, {{ dotType }}(1.0), error);
24
+ return DotAccumulator(hi, fma({{ dotType }}(-1.0), fma({{ dotType }}(-1.0), sum, hi), error));
25
+ }
26
+ fn dot_value(acc: DotAccumulator) -> {{ dotType }} {
27
+ return fma(acc.hi, {{ dotType }}(1.0), acc.lo);
28
+ }
29
 
30
  const HIDDEN: u32 = {{ hidden }}u;
31
  const INTER: u32 = {{ inter }}u;
 
33
  {% if topK > 1 %}
34
  const TOP_K: u32 = {{ topK }}u;
35
  {% endif %}
36
+ {% set qw = "fc2" %}
37
+ {% set qwRows = "HIDDEN" %}
38
+ {% set qwPackedCols = fc2PackedCols %}
39
+ {% set qwBlocks = fc2ScaleBlocks %}
40
  // Quantized expert weights use unsigned codes offset by 2^(bits-1), so a
41
  // weight is (code - MIDPOINT) * scale. Four-bit codes are packed low-nibble
42
  // first along the reduction axis, two codes per stored word.
43
  const MIDPOINT: f32 = f32({{ quantMidpoint }});
44
+ const {{ qw | upper }}_PACKED_COLS: u32 = {{ qwPackedCols }}u;
45
  {% if not colWiseScales %}
46
+ // Block-wise scales ({{ qw }}_scales rank 3): one scale per QUANT_BLOCK columns of the reduction
47
  // axis. Column-wise scales (block_size 0) are the rank-2 single-scale-per-row case below.
48
  const QUANT_BLOCK: u32 = {{ quantBlockSize }}u;
49
+ const {{ qw | upper }}_BLOCKS: u32 = {{ qwBlocks }}u;
50
  {% endif %}
51
 
52
+ fn {{ qw }}_weight(expert: u32, row: u32, col: u32) -> f32 {
53
+ let base = expert * {{ qwRows }} + row;
54
  {% if packSize == 2 %}
55
+ let word = {{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col / 2u];
56
  let code = f32((word >> ((col & 1u) * 4u)) & 15u);
57
  {% else %}
58
+ let code = f32({{ qw }}_experts_weights[base * {{ qw | upper }}_PACKED_COLS + col] & 255u);
59
  {% endif %}
60
  {% if not colWiseScales %}
61
+ return (code - MIDPOINT) * {{ qw }}_scales[base * {{ qw | upper }}_BLOCKS + col / QUANT_BLOCK];
62
  {% else %}
63
+ return (code - MIDPOINT) * {{ qw }}_scales[base];
64
  {% endif %}
65
  }
66
 
 
67
  @compute @workgroup_size(WG, 1, 1)
68
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
69
+ {{ flat_index_2d("WG", "index", "") }}
 
 
70
  let total = params.tokenCount * HIDDEN;
71
  if (index >= total) {
72
  return;
 
83
  var acc = 0.0;
84
  for (var j = 0u; j < TOP_K; j = j + 1u) {
85
  let expert = route_expert[token * TOP_K + j];
86
+ var part = DotAccumulator(0.0, 0.0);
87
  for (var i: u32 = 0u; i < INTER; i = i + 1u) {
88
+ part = dot_accumulate(part, hidden_act[(local_token * TOP_K + j) * INTER + i], fc2_weight(expert, out_col, i));
89
  }
90
+ acc = acc + dot_value(part) * route_mix[token * TOP_K + j];
91
  }
92
 
93
  output[token * HIDDEN + out_col] = acc;
 
95
  let expert = route_expert[token];
96
  let mix = route_mix[token];
97
 
98
+ var acc = DotAccumulator(0.0, 0.0);
99
  for (var i: u32 = 0u; i < INTER; i = i + 1u) {
100
+ acc = dot_accumulate(acc, hidden_act[local_token * INTER + i], fc2_weight(expert, out_col, i));
101
  }
102
 
103
+ output[token * HIDDEN + out_col] = dot_value(acc) * mix;
104
  {% endif %}
105
  }
build/webgpu/qmoe-route-stage.wgsl.jinja CHANGED
@@ -1,6 +1,13 @@
 
 
 
 
 
 
 
 
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 %}
@@ -54,7 +61,6 @@ fn topk_route(token: u32) -> TopKRoute {
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.
@@ -75,17 +81,11 @@ fn top1_route(token: u32) -> Top1Route {
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
- // 2D-folded flat index: gid.y carries the high bits past the per-axis dispatch fold width.
84
- // Reduces to gid.x when the dispatch does not fold.
85
- let token = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
86
- if (token >= TOKENS) {
87
- return;
88
- }
89
 
90
  {% if topK > 1 %}
91
  // Slot j of each token's TOP_K-wide run holds its j-th ranked expert and
 
1
+ {% macro flat_index_2d(workgroupSize, name="i", bound="params.count", guardInline=false) %}
2
+ {% set wgTerm = workgroupSize ~ "u" if workgroupSize is number else workgroupSize %}
3
+ // 2D-folded flat index: gid.y carries the high bits past the dispatch's
4
+ // per-axis workgroup fold width.
5
+ let {{ name }} = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ wgTerm }};
6
+ if ({{ name }} >= {{ bound }}) {
7
+ return;
8
+ }{% endmacro %}
9
  {{ env.wgsl.resourceDeclarations }}
10
 
 
11
  const EXPERTS: u32 = {{ experts }}u;
12
  const WG: u32 = {{ workgroupSize }}u;
13
  {% if topK > 1 %}
 
61
  }
62
  return result;
63
  }
 
64
  {% else %}
65
  // Serial top-1 selection over `(logit, expert_index)` pairs ordered descending,
66
  // so equal logits select the higher expert index.
 
81
  }
82
  return result;
83
  }
 
84
  {% endif %}
85
 
86
  @compute @workgroup_size(WG, 1, 1)
87
  fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
88
+ {{ flat_index_2d("WG", "token", "params.tokenCount") }}
 
 
 
 
 
89
 
90
  {% if topK > 1 %}
91
  // Slot j of each token's TOP_K-wide run holds its j-th ranked expert and
build/webgpu/test.json CHANGED
The diff for this file is too large to render. See raw diff