Xenova HF Staff commited on
Commit
6d3894e
·
verified ·
1 Parent(s): 0ef25e5

sync 2e7068faf55e

Browse files
README.md CHANGED
@@ -1,3 +1,91 @@
1
  ---
 
2
  license: apache-2.0
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ library_name: kernels
3
  license: apache-2.0
4
+ tags:
5
+ - kernel
6
+ - webgpu
7
+ - wgsl
8
  ---
9
+ # com.microsoft.FusedConv
10
+
11
+ `com.microsoft` · ONNX Runtime contrib operator · contrib since_version 1
12
+
13
+ ## Description
14
+
15
+ Applies an N-dimensional convolution with optional bias `B` and residual `Z`, followed by an optional fused activation. Omitting `activation` leaves the convolution result unchanged. Supported activations are `Relu`, `LeakyRelu`, `Sigmoid`, `Tanh`, `HardSigmoid`, `HardSwish`, and `Clip`; other schema-permitted activation strings are not implemented. The implementation supports one to three spatial dimensions and float16 or float32; higher spatial ranks and float64 are not implemented.
16
+
17
+ See the [ONNX Runtime `FusedConv` contrib-operator spec](https://github.com/microsoft/onnxruntime/blob/main/docs/ContribOperators.md#com.microsoft.FusedConv) for the reference semantics.
18
+
19
+ ## Inputs
20
+
21
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
22
+ | --- | --- | --- | --- | --- | --- | --- |
23
+ | `X` | `x` | `T` | — | — | Input data tensor of shape `(N, C, D1, ..., Dn)` for one to three spatial dimensions. | required |
24
+ | `W` | `w` | `T` | — | — | Convolution filter tensor of shape `(M, C/group, k1, ..., kn)`, with the same spatial rank as `X`. | required |
25
+ | `B` | `bias` | `T` | `1` | — | Optional 1-D bias tensor of length `out_channels`, broadcast-added to each output channel. | optional |
26
+ | `Z` | `zResidual` | `T` | same as `X` | — | Optional residual tensor with the same shape as the output `Y`, added before the activation. | optional |
27
+
28
+ ## Outputs
29
+
30
+ | Name | Bind key | Logical dtype | Rank | Shape | Description | Presence |
31
+ | --- | --- | --- | --- | --- | --- | --- |
32
+ | `Y` | `y` | `T` | same as `X` | derived; see description | Output feature map tensor after convolution, optional bias/residual addition, and the fused activation. | required |
33
+
34
+ ## Attributes
35
+
36
+ Attributes and default values (overridable per request):
37
+
38
+ | Attribute | Default | Description |
39
+ | --- | --- | --- |
40
+ | `auto_pad` | `"NOTSET"` | Automatic padding mode. `NOTSET` uses `pads`; `SAME_UPPER` and `SAME_LOWER` choose padding so each output spatial size is `ceil(input / stride)`; `VALID` uses no padding. |
41
+ | `group` | `1` | Number of groups that input and output channels are split into; defaults to 1. |
42
+ | `activation` | — | Optional fused activation name: `Relu`, `LeakyRelu`, `Sigmoid`, `Tanh`, `HardSigmoid`, `HardSwish`, or `Clip`. Omission applies no activation. |
43
+ | `activation_params` | — | Positional parameters for the fused activation: exactly `[alpha]` is required for `LeakyRelu`, and exactly `[alpha, beta]` or `[min, max]` is required for `HardSigmoid` or `Clip`, respectively. Parameter-free activations ignore this attribute. |
44
+ | `dilations` | — | Optional dilation factors, one positive integer per spatial axis. Omission means all ones. |
45
+ | `kernel_shape` | — | Optional kernel shape, one positive integer per spatial axis. When present, it must match the spatial dimensions of the weight tensor; omission infers the shape from the weights. |
46
+ | `pads` | — | Optional explicit padding in ONNX order `[begin_axis_0, ..., begin_axis_n, end_axis_0, ..., end_axis_n]`. Omission means all zeros; it cannot be combined with an automatic padding mode. |
47
+ | `strides` | — | Optional stride factors, one positive integer per spatial axis. Omission means all ones. |
48
+
49
+ ## Type constraints
50
+
51
+ | Variable | Allowed dtypes |
52
+ | --- | --- |
53
+ | `T` | `float32`, `float16` |
54
+
55
+ ## Device requirements
56
+
57
+ Some implementation variants require `subgroup-matrix` and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
58
+
59
+ ## Files
60
+
61
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, provenance)
62
+ - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
63
+ - [`test.json`](build/webgpu/test.json) — correctness cases
64
+ - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
65
+ - [`conv-1x1-gemm-tiled-reg.wgsl.jinja`](build/webgpu/conv-1x1-gemm-tiled-reg.wgsl.jinja)
66
+ - [`conv-1x1-gemm-tiled.wgsl.jinja`](build/webgpu/conv-1x1-gemm-tiled.wgsl.jinja)
67
+ - [`conv-1x1-subgroup-matrix.wgsl.jinja`](build/webgpu/conv-1x1-subgroup-matrix.wgsl.jinja)
68
+ - [`conv-direct-nd.wgsl.jinja`](build/webgpu/conv-direct-nd.wgsl.jinja)
69
+ - [`conv-direct-unrolled.wgsl.jinja`](build/webgpu/conv-direct-unrolled.wgsl.jinja)
70
+ - [`conv-im2col-nchw.wgsl.jinja`](build/webgpu/conv-im2col-nchw.wgsl.jinja)
71
+ - [`conv1d-tiled-reg.wgsl.jinja`](build/webgpu/conv1d-tiled-reg.wgsl.jinja)
72
+ - [`conv2d-grouped-large-w4.wgsl.jinja`](build/webgpu/conv2d-grouped-large-w4.wgsl.jinja)
73
+
74
+ ## Use with `@huggingface/kernels`
75
+
76
+ The loader derives every required output's shape and logical dtype from the manifest contract and this call.
77
+ It then allocates the result tensors automatically.
78
+
79
+ The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
80
+
81
+ Replace each `*Data` placeholder with a typed array containing the corresponding input data.
82
+
83
+ ```js
84
+ import { getKernel } from "@huggingface/kernels";
85
+
86
+ const kernel = await getKernel("webgpu-kernels/com.microsoft.FusedConv", { version: 1 });
87
+ const { y } = await kernel({
88
+ x: { data: xData, shape: [1, 32, 8, 8] },
89
+ w: { data: wData, shape: [32, 32, 1, 1] },
90
+ });
91
+ ```
build/webgpu/bench.json ADDED
@@ -0,0 +1,922 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "op": "com.microsoft.FusedConv",
3
+ "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
+ "cases": [
5
+ {
6
+ "name": "fusedconv-f32-b1c16m32-32x32-k3",
7
+ "preset": "smoke",
8
+ "vars": {
9
+ "batch": 1,
10
+ "inChannels": 16,
11
+ "outChannels": 32,
12
+ "inH": 32,
13
+ "inW": 32,
14
+ "kernelH": 3,
15
+ "kernelW": 3,
16
+ "strideH": 1,
17
+ "strideW": 1,
18
+ "padH": 1,
19
+ "padW": 1
20
+ },
21
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
22
+ "inputs": {
23
+ "x": { "shape": [1, 16, 32, 32], "dtype": "float32", "dist": "normal", "seed": 112, "scale": 0.2 },
24
+ "w": { "shape": [32, 16, 3, 3], "dtype": "float32", "dist": "normal", "seed": 113, "scale": 0.1 }
25
+ },
26
+ "outputs": { "y": { "shape": [1, 32, 32, 32], "dtype": "float32" } },
27
+ "bench": {
28
+ "primary": true,
29
+ "metrics": [
30
+ {
31
+ "type": "gflops",
32
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
33
+ }
34
+ ]
35
+ }
36
+ },
37
+ {
38
+ "name": "fusedconv-f32-bias-b1c16m32-32x32-k3",
39
+ "preset": "smoke",
40
+ "vars": {
41
+ "batch": 1,
42
+ "inChannels": 16,
43
+ "outChannels": 32,
44
+ "inH": 32,
45
+ "inW": 32,
46
+ "kernelH": 3,
47
+ "kernelW": 3,
48
+ "strideH": 1,
49
+ "strideW": 1,
50
+ "padH": 1,
51
+ "padW": 1
52
+ },
53
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
54
+ "inputs": {
55
+ "x": { "shape": [1, 16, 32, 32], "dtype": "float32", "dist": "normal", "seed": 113, "scale": 0.2 },
56
+ "w": { "shape": [32, 16, 3, 3], "dtype": "float32", "dist": "normal", "seed": 114, "scale": 0.1 },
57
+ "bias": { "shape": [32], "dtype": "float32", "dist": "normal", "seed": 115, "scale": 0.05 }
58
+ },
59
+ "outputs": { "y": { "shape": [1, 32, 32, 32], "dtype": "float32" } },
60
+ "bench": {
61
+ "primary": true,
62
+ "metrics": [
63
+ {
64
+ "type": "gflops",
65
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
66
+ }
67
+ ]
68
+ }
69
+ },
70
+ {
71
+ "name": "fused-flux2vae-dec-mid-512c-64x64-k3",
72
+ "preset": "model",
73
+ "vars": {
74
+ "batch": 1,
75
+ "inChannels": 512,
76
+ "outChannels": 512,
77
+ "inH": 64,
78
+ "inW": 64,
79
+ "kernelH": 3,
80
+ "kernelW": 3,
81
+ "strideH": 1,
82
+ "strideW": 1,
83
+ "padH": 1,
84
+ "padW": 1
85
+ },
86
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
87
+ "inputs": {
88
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float32", "dist": "normal", "seed": 340, "scale": 0.2 },
89
+ "w": { "shape": [512, 512, 3, 3], "dtype": "float32", "dist": "normal", "seed": 341, "scale": 0.02 }
90
+ },
91
+ "outputs": { "y": { "shape": [1, 512, 64, 64], "dtype": "float32" } },
92
+ "bench": {
93
+ "metrics": [
94
+ {
95
+ "type": "gflops",
96
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
97
+ }
98
+ ]
99
+ }
100
+ },
101
+ {
102
+ "name": "fused-flux2vae-dec-up1-512c-128x128-k3",
103
+ "preset": "model",
104
+ "vars": {
105
+ "batch": 1,
106
+ "inChannels": 512,
107
+ "outChannels": 512,
108
+ "inH": 128,
109
+ "inW": 128,
110
+ "kernelH": 3,
111
+ "kernelW": 3,
112
+ "strideH": 1,
113
+ "strideW": 1,
114
+ "padH": 1,
115
+ "padW": 1
116
+ },
117
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
118
+ "inputs": {
119
+ "x": { "shape": [1, 512, 128, 128], "dtype": "float32", "dist": "normal", "seed": 342, "scale": 0.2 },
120
+ "w": { "shape": [512, 512, 3, 3], "dtype": "float32", "dist": "normal", "seed": 343, "scale": 0.02 }
121
+ },
122
+ "outputs": { "y": { "shape": [1, 512, 128, 128], "dtype": "float32" } },
123
+ "bench": {
124
+ "metrics": [
125
+ {
126
+ "type": "gflops",
127
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
128
+ }
129
+ ]
130
+ }
131
+ },
132
+ {
133
+ "name": "fused-flux2vae-attn-proj-512c-64x64-k1",
134
+ "preset": "model",
135
+ "vars": {
136
+ "batch": 1,
137
+ "inChannels": 512,
138
+ "outChannels": 512,
139
+ "inH": 64,
140
+ "inW": 64,
141
+ "kernelH": 1,
142
+ "kernelW": 1,
143
+ "strideH": 1,
144
+ "strideW": 1,
145
+ "padH": 0,
146
+ "padW": 0
147
+ },
148
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
149
+ "inputs": {
150
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float32", "dist": "normal", "seed": 344, "scale": 0.2 },
151
+ "w": { "shape": [512, 512, 1, 1], "dtype": "float32", "dist": "normal", "seed": 345, "scale": 0.02 }
152
+ },
153
+ "outputs": { "y": { "shape": [1, 512, 64, 64], "dtype": "float32" } },
154
+ "bench": {
155
+ "metrics": [
156
+ {
157
+ "type": "gflops",
158
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
159
+ }
160
+ ]
161
+ }
162
+ },
163
+ {
164
+ "name": "fused-flux2vae-attn-proj-bias-512c-64x64-k1",
165
+ "preset": "model",
166
+ "vars": {
167
+ "batch": 1,
168
+ "inChannels": 512,
169
+ "outChannels": 512,
170
+ "inH": 64,
171
+ "inW": 64,
172
+ "kernelH": 1,
173
+ "kernelW": 1,
174
+ "strideH": 1,
175
+ "strideW": 1,
176
+ "padH": 0,
177
+ "padW": 0
178
+ },
179
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
180
+ "inputs": {
181
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float32", "dist": "normal", "seed": 346, "scale": 0.2 },
182
+ "w": { "shape": [512, 512, 1, 1], "dtype": "float32", "dist": "normal", "seed": 347, "scale": 0.02 },
183
+ "bias": { "shape": [512], "dtype": "float32", "dist": "normal", "seed": 348, "scale": 0.05 }
184
+ },
185
+ "outputs": { "y": { "shape": [1, 512, 64, 64], "dtype": "float32" } },
186
+ "bench": {
187
+ "metrics": [
188
+ {
189
+ "type": "gflops",
190
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
191
+ }
192
+ ]
193
+ }
194
+ },
195
+ {
196
+ "name": "fusedconv-f32-1x1-b1c256m256-32x32",
197
+ "preset": "smoke",
198
+ "vars": {
199
+ "batch": 1,
200
+ "inChannels": 256,
201
+ "outChannels": 256,
202
+ "inH": 32,
203
+ "inW": 32,
204
+ "kernelH": 1,
205
+ "kernelW": 1,
206
+ "strideH": 1,
207
+ "strideW": 1,
208
+ "padH": 0,
209
+ "padW": 0
210
+ },
211
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
212
+ "inputs": {
213
+ "x": { "shape": [1, 256, 32, 32], "dtype": "float32", "dist": "normal", "seed": 350, "scale": 0.2 },
214
+ "w": { "shape": [256, 256, 1, 1], "dtype": "float32", "dist": "normal", "seed": 351, "scale": 0.02 }
215
+ },
216
+ "outputs": { "y": { "shape": [1, 256, 32, 32], "dtype": "float32" } },
217
+ "bench": {
218
+ "metrics": [
219
+ {
220
+ "type": "gflops",
221
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
222
+ }
223
+ ]
224
+ }
225
+ },
226
+ {
227
+ "name": "fusedconv-f32-1x1-bias-b1c256m256-32x32",
228
+ "preset": "smoke",
229
+ "provenance": {
230
+ "source": "authored for performance coverage",
231
+ "notes": "Bias twin of the paired non-bias case at the same 64 register tiles. No bias case sat in the 64-127 tile band, so the bias register-tile floor had no benchmark on either side of it."
232
+ },
233
+ "vars": {
234
+ "batch": 1,
235
+ "inChannels": 256,
236
+ "outChannels": 256,
237
+ "inH": 32,
238
+ "inW": 32,
239
+ "kernelH": 1,
240
+ "kernelW": 1,
241
+ "strideH": 1,
242
+ "strideW": 1,
243
+ "padH": 0,
244
+ "padW": 0
245
+ },
246
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
247
+ "inputs": {
248
+ "x": { "shape": [1, 256, 32, 32], "dtype": "float32", "dist": "normal", "seed": 3521, "scale": 0.2 },
249
+ "w": { "shape": [256, 256, 1, 1], "dtype": "float32", "dist": "normal", "seed": 3522, "scale": 0.02 },
250
+ "bias": { "shape": [256], "dtype": "float32", "dist": "normal", "seed": 3523, "scale": 0.05 }
251
+ },
252
+ "outputs": { "y": { "shape": [1, 256, 32, 32], "dtype": "float32" } },
253
+ "bench": {
254
+ "metrics": [
255
+ {
256
+ "type": "gflops",
257
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
258
+ }
259
+ ]
260
+ }
261
+ },
262
+ {
263
+ "name": "fusedconv-f32-im2col-b1c32m64-32x32-k3",
264
+ "preset": "smoke",
265
+ "vars": {
266
+ "batch": 1,
267
+ "inChannels": 32,
268
+ "outChannels": 64,
269
+ "inH": 32,
270
+ "inW": 32,
271
+ "kernelH": 3,
272
+ "kernelW": 3,
273
+ "strideH": 1,
274
+ "strideW": 1,
275
+ "padH": 1,
276
+ "padW": 1
277
+ },
278
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
279
+ "inputs": {
280
+ "x": { "shape": [1, 32, 32, 32], "dtype": "float32", "dist": "normal", "seed": 352, "scale": 0.2 },
281
+ "w": { "shape": [64, 32, 3, 3], "dtype": "float32", "dist": "normal", "seed": 353, "scale": 0.02 }
282
+ },
283
+ "outputs": { "y": { "shape": [1, 64, 32, 32], "dtype": "float32" } },
284
+ "bench": {
285
+ "metrics": [
286
+ {
287
+ "type": "gflops",
288
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
289
+ }
290
+ ]
291
+ }
292
+ },
293
+ {
294
+ "name": "fusedconv-f16-1x1-b1c512m512-64x64",
295
+ "preset": "model",
296
+ "vars": {
297
+ "batch": 1,
298
+ "inChannels": 512,
299
+ "outChannels": 512,
300
+ "inH": 64,
301
+ "inW": 64,
302
+ "kernelH": 1,
303
+ "kernelW": 1,
304
+ "strideH": 1,
305
+ "strideW": 1,
306
+ "padH": 0,
307
+ "padW": 0
308
+ },
309
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
310
+ "inputs": {
311
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float16", "dist": "normal", "seed": 354, "scale": 0.2 },
312
+ "w": { "shape": [512, 512, 1, 1], "dtype": "float16", "dist": "normal", "seed": 355, "scale": 0.02 }
313
+ },
314
+ "outputs": { "y": { "shape": [1, 512, 64, 64], "dtype": "float16" } },
315
+ "bench": {
316
+ "metrics": [
317
+ {
318
+ "type": "gflops",
319
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
320
+ }
321
+ ]
322
+ }
323
+ },
324
+ {
325
+ "name": "fusedconv-f16-b1c512m512-64x64-k3",
326
+ "preset": "model",
327
+ "vars": {
328
+ "batch": 1,
329
+ "inChannels": 512,
330
+ "outChannels": 512,
331
+ "inH": 64,
332
+ "inW": 64,
333
+ "kernelH": 3,
334
+ "kernelW": 3,
335
+ "strideH": 1,
336
+ "strideW": 1,
337
+ "padH": 1,
338
+ "padW": 1
339
+ },
340
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
341
+ "inputs": {
342
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float16", "dist": "normal", "seed": 356, "scale": 0.2 },
343
+ "w": { "shape": [512, 512, 3, 3], "dtype": "float16", "dist": "normal", "seed": 357, "scale": 0.02 }
344
+ },
345
+ "outputs": { "y": { "shape": [1, 512, 64, 64], "dtype": "float16" } },
346
+ "bench": {
347
+ "metrics": [
348
+ {
349
+ "type": "gflops",
350
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
351
+ }
352
+ ]
353
+ }
354
+ },
355
+ {
356
+ "name": "fusedconv-f32-b1c64m128-56x56-k3s2",
357
+ "preset": "model",
358
+ "vars": {
359
+ "batch": 1,
360
+ "inChannels": 64,
361
+ "outChannels": 128,
362
+ "inH": 56,
363
+ "inW": 56,
364
+ "kernelH": 3,
365
+ "kernelW": 3,
366
+ "strideH": 2,
367
+ "strideW": 2,
368
+ "padH": 1,
369
+ "padW": 1
370
+ },
371
+ "attrs": { "activation": "Relu", "strides": [2, 2], "pads": [1, 1, 1, 1] },
372
+ "inputs": {
373
+ "x": { "shape": [1, 64, 56, 56], "dtype": "float32", "dist": "normal", "seed": 358, "scale": 0.2 },
374
+ "w": { "shape": [128, 64, 3, 3], "dtype": "float32", "dist": "normal", "seed": 359, "scale": 0.02 }
375
+ },
376
+ "outputs": { "y": { "shape": [1, 128, 28, 28], "dtype": "float32" } },
377
+ "bench": {
378
+ "metrics": [
379
+ {
380
+ "type": "gflops",
381
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
382
+ }
383
+ ]
384
+ }
385
+ },
386
+ {
387
+ "name": "fusedconv-f32-group4-b1c64m64-32x32-k3",
388
+ "preset": "model",
389
+ "vars": {
390
+ "batch": 1,
391
+ "inChannels": 64,
392
+ "outChannels": 64,
393
+ "inH": 32,
394
+ "inW": 32,
395
+ "kernelH": 3,
396
+ "kernelW": 3,
397
+ "strideH": 1,
398
+ "strideW": 1,
399
+ "padH": 1,
400
+ "padW": 1
401
+ },
402
+ "attrs": { "group": 4, "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
403
+ "inputs": {
404
+ "x": { "shape": [1, 64, 32, 32], "dtype": "float32", "dist": "normal", "seed": 360, "scale": 0.2 },
405
+ "w": { "shape": [64, 16, 3, 3], "dtype": "float32", "dist": "normal", "seed": 361, "scale": 0.02 }
406
+ },
407
+ "outputs": { "y": { "shape": [1, 64, 32, 32], "dtype": "float32" } },
408
+ "bench": {
409
+ "metrics": [
410
+ {
411
+ "type": "gflops",
412
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
413
+ }
414
+ ]
415
+ }
416
+ },
417
+ {
418
+ "name": "fusedconv-f32-stem-3ch-224-k7s2",
419
+ "preset": "model",
420
+ "vars": {
421
+ "batch": 1,
422
+ "inChannels": 3,
423
+ "outChannels": 64,
424
+ "inH": 224,
425
+ "inW": 224,
426
+ "kernelH": 7,
427
+ "kernelW": 7,
428
+ "strideH": 2,
429
+ "strideW": 2,
430
+ "padH": 3,
431
+ "padW": 3
432
+ },
433
+ "attrs": { "activation": "Relu", "strides": [2, 2], "pads": [3, 3, 3, 3] },
434
+ "inputs": {
435
+ "x": { "shape": [1, 3, 224, 224], "dtype": "float32", "dist": "normal", "seed": 1458, "scale": 0.2 },
436
+ "w": { "shape": [64, 3, 7, 7], "dtype": "float32", "dist": "normal", "seed": 1459, "scale": 0.02 }
437
+ },
438
+ "outputs": { "y": { "shape": [1, 64, 112, 112], "dtype": "float32" } },
439
+ "bench": {
440
+ "metrics": [
441
+ {
442
+ "type": "gflops",
443
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
444
+ }
445
+ ]
446
+ }
447
+ },
448
+ {
449
+ "name": "fusedconv-f32-resnet-stage-256c-14x14-k3",
450
+ "preset": "model",
451
+ "vars": {
452
+ "batch": 1,
453
+ "inChannels": 256,
454
+ "outChannels": 256,
455
+ "inH": 14,
456
+ "inW": 14,
457
+ "kernelH": 3,
458
+ "kernelW": 3,
459
+ "strideH": 1,
460
+ "strideW": 1,
461
+ "padH": 1,
462
+ "padW": 1
463
+ },
464
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
465
+ "inputs": {
466
+ "x": { "shape": [1, 256, 14, 14], "dtype": "float32", "dist": "normal", "seed": 2458, "scale": 0.2 },
467
+ "w": { "shape": [256, 256, 3, 3], "dtype": "float32", "dist": "normal", "seed": 2459, "scale": 0.02 }
468
+ },
469
+ "outputs": { "y": { "shape": [1, 256, 14, 14], "dtype": "float32" } },
470
+ "bench": {
471
+ "metrics": [
472
+ {
473
+ "type": "gflops",
474
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
475
+ }
476
+ ]
477
+ }
478
+ },
479
+ {
480
+ "name": "fusedconv-1x1-tinyN-c256m256-8x8",
481
+ "preset": "smoke",
482
+ "vars": {
483
+ "batch": 1,
484
+ "inChannels": 256,
485
+ "outChannels": 256,
486
+ "inH": 8,
487
+ "inW": 8,
488
+ "kernelH": 1,
489
+ "kernelW": 1,
490
+ "strideH": 1,
491
+ "strideW": 1,
492
+ "padH": 0,
493
+ "padW": 0
494
+ },
495
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
496
+ "inputs": {
497
+ "x": { "shape": [1, 256, 8, 8], "dtype": "float32", "dist": "normal", "seed": 951, "scale": 0.2 },
498
+ "w": { "shape": [256, 256, 1, 1], "dtype": "float32", "dist": "normal", "seed": 1642, "scale": 0.05 }
499
+ },
500
+ "outputs": { "y": { "shape": [1, 256, 8, 8], "dtype": "float32" } },
501
+ "bench": {
502
+ "primary": true,
503
+ "metrics": [
504
+ {
505
+ "type": "gflops",
506
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
507
+ }
508
+ ]
509
+ }
510
+ },
511
+ {
512
+ "name": "fusedconv-1x1-bigN-c256m256-64x64",
513
+ "preset": "smoke",
514
+ "vars": {
515
+ "batch": 1,
516
+ "inChannels": 256,
517
+ "outChannels": 256,
518
+ "inH": 64,
519
+ "inW": 64,
520
+ "kernelH": 1,
521
+ "kernelW": 1,
522
+ "strideH": 1,
523
+ "strideW": 1,
524
+ "padH": 0,
525
+ "padW": 0
526
+ },
527
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
528
+ "inputs": {
529
+ "x": { "shape": [1, 256, 64, 64], "dtype": "float32", "dist": "normal", "seed": 7277, "scale": 0.2 },
530
+ "w": { "shape": [256, 256, 1, 1], "dtype": "float32", "dist": "normal", "seed": 1530, "scale": 0.05 }
531
+ },
532
+ "outputs": { "y": { "shape": [1, 256, 64, 64], "dtype": "float32" } },
533
+ "bench": {
534
+ "metrics": [
535
+ {
536
+ "type": "gflops",
537
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
538
+ }
539
+ ]
540
+ }
541
+ },
542
+ {
543
+ "name": "fusedconv-3x3-unalignedK-c48m64-32x32",
544
+ "preset": "smoke",
545
+ "vars": {
546
+ "batch": 1,
547
+ "inChannels": 48,
548
+ "outChannels": 64,
549
+ "inH": 32,
550
+ "inW": 32,
551
+ "kernelH": 3,
552
+ "kernelW": 3,
553
+ "strideH": 1,
554
+ "strideW": 1,
555
+ "padH": 1,
556
+ "padW": 1
557
+ },
558
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
559
+ "inputs": {
560
+ "x": { "shape": [1, 48, 32, 32], "dtype": "float32", "dist": "normal", "seed": 3152, "scale": 0.2 },
561
+ "w": { "shape": [64, 48, 3, 3], "dtype": "float32", "dist": "normal", "seed": 7809, "scale": 0.05 }
562
+ },
563
+ "outputs": { "y": { "shape": [1, 64, 32, 32], "dtype": "float32" } },
564
+ "bench": {
565
+ "metrics": [
566
+ {
567
+ "type": "gflops",
568
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
569
+ }
570
+ ]
571
+ }
572
+ },
573
+ {
574
+ "name": "fusedconv-3x3-alignedK-c64m64-32x32",
575
+ "preset": "smoke",
576
+ "vars": {
577
+ "batch": 1,
578
+ "inChannels": 64,
579
+ "outChannels": 64,
580
+ "inH": 32,
581
+ "inW": 32,
582
+ "kernelH": 3,
583
+ "kernelW": 3,
584
+ "strideH": 1,
585
+ "strideW": 1,
586
+ "padH": 1,
587
+ "padW": 1
588
+ },
589
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
590
+ "inputs": {
591
+ "x": { "shape": [1, 64, 32, 32], "dtype": "float32", "dist": "normal", "seed": 7770, "scale": 0.2 },
592
+ "w": { "shape": [64, 64, 3, 3], "dtype": "float32", "dist": "normal", "seed": 2676, "scale": 0.05 }
593
+ },
594
+ "outputs": { "y": { "shape": [1, 64, 32, 32], "dtype": "float32" } },
595
+ "bench": {
596
+ "metrics": [
597
+ {
598
+ "type": "gflops",
599
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
600
+ }
601
+ ]
602
+ }
603
+ },
604
+ {
605
+ "name": "fusedconv-3x3-depthwise-c256-32x32",
606
+ "preset": "smoke",
607
+ "vars": {
608
+ "batch": 1,
609
+ "inChannels": 256,
610
+ "outChannels": 256,
611
+ "inH": 32,
612
+ "inW": 32,
613
+ "kernelH": 3,
614
+ "kernelW": 3,
615
+ "strideH": 1,
616
+ "strideW": 1,
617
+ "padH": 1,
618
+ "padW": 1
619
+ },
620
+ "attrs": { "group": 256, "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
621
+ "inputs": {
622
+ "x": { "shape": [1, 256, 32, 32], "dtype": "float32", "dist": "normal", "seed": 8532, "scale": 0.2 },
623
+ "w": { "shape": [256, 1, 3, 3], "dtype": "float32", "dist": "normal", "seed": 7192, "scale": 0.05 }
624
+ },
625
+ "outputs": { "y": { "shape": [1, 256, 32, 32], "dtype": "float32" } },
626
+ "bench": {
627
+ "metrics": [
628
+ {
629
+ "type": "gflops",
630
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
631
+ }
632
+ ]
633
+ }
634
+ },
635
+ {
636
+ "name": "fusedconv-3x3-dense-c256m256-32x32",
637
+ "preset": "smoke",
638
+ "vars": {
639
+ "batch": 1,
640
+ "inChannels": 256,
641
+ "outChannels": 256,
642
+ "inH": 32,
643
+ "inW": 32,
644
+ "kernelH": 3,
645
+ "kernelW": 3,
646
+ "strideH": 1,
647
+ "strideW": 1,
648
+ "padH": 1,
649
+ "padW": 1
650
+ },
651
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
652
+ "inputs": {
653
+ "x": { "shape": [1, 256, 32, 32], "dtype": "float32", "dist": "normal", "seed": 839, "scale": 0.2 },
654
+ "w": { "shape": [256, 256, 3, 3], "dtype": "float32", "dist": "normal", "seed": 1375, "scale": 0.05 }
655
+ },
656
+ "outputs": { "y": { "shape": [1, 256, 32, 32], "dtype": "float32" } },
657
+ "bench": {
658
+ "metrics": [
659
+ {
660
+ "type": "gflops",
661
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
662
+ }
663
+ ]
664
+ }
665
+ },
666
+ {
667
+ "name": "fusedconv-3x3-dense-bias-c256m256-32x32",
668
+ "preset": "smoke",
669
+ "provenance": {
670
+ "source": "authored for performance coverage",
671
+ "notes": "Bias twin of the paired non-bias case at the same 64 register tiles. No bias case sat in the 64-127 tile band, so the bias register-tile floor had no benchmark on either side of it."
672
+ },
673
+ "vars": {
674
+ "batch": 1,
675
+ "inChannels": 256,
676
+ "outChannels": 256,
677
+ "inH": 32,
678
+ "inW": 32,
679
+ "kernelH": 3,
680
+ "kernelW": 3,
681
+ "strideH": 1,
682
+ "strideW": 1,
683
+ "padH": 1,
684
+ "padW": 1
685
+ },
686
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
687
+ "inputs": {
688
+ "x": { "shape": [1, 256, 32, 32], "dtype": "float32", "dist": "normal", "seed": 3531, "scale": 0.2 },
689
+ "w": { "shape": [256, 256, 3, 3], "dtype": "float32", "dist": "normal", "seed": 3532, "scale": 0.05 },
690
+ "bias": { "shape": [256], "dtype": "float32", "dist": "normal", "seed": 3533, "scale": 0.05 }
691
+ },
692
+ "outputs": { "y": { "shape": [1, 256, 32, 32], "dtype": "float32" } },
693
+ "bench": {
694
+ "metrics": [
695
+ {
696
+ "type": "gflops",
697
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
698
+ }
699
+ ]
700
+ }
701
+ },
702
+ {
703
+ "name": "fusedconv-stem-cin3-k7s2-224",
704
+ "preset": "smoke",
705
+ "vars": {
706
+ "batch": 1,
707
+ "inChannels": 3,
708
+ "outChannels": 64,
709
+ "inH": 224,
710
+ "inW": 224,
711
+ "kernelH": 7,
712
+ "kernelW": 7,
713
+ "strideH": 2,
714
+ "strideW": 2,
715
+ "padH": 3,
716
+ "padW": 3
717
+ },
718
+ "attrs": { "activation": "Relu", "strides": [2, 2], "pads": [3, 3, 3, 3] },
719
+ "inputs": {
720
+ "x": { "shape": [1, 3, 224, 224], "dtype": "float32", "dist": "normal", "seed": 1633, "scale": 0.2 },
721
+ "w": { "shape": [64, 3, 7, 7], "dtype": "float32", "dist": "normal", "seed": 3717, "scale": 0.05 }
722
+ },
723
+ "outputs": { "y": { "shape": [1, 64, 112, 112], "dtype": "float32" } },
724
+ "bench": {
725
+ "metrics": [
726
+ {
727
+ "type": "gflops",
728
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
729
+ }
730
+ ]
731
+ }
732
+ },
733
+ {
734
+ "name": "fusedconv-mid-cin32-k3-112",
735
+ "preset": "smoke",
736
+ "vars": {
737
+ "batch": 1,
738
+ "inChannels": 32,
739
+ "outChannels": 64,
740
+ "inH": 112,
741
+ "inW": 112,
742
+ "kernelH": 3,
743
+ "kernelW": 3,
744
+ "strideH": 1,
745
+ "strideW": 1,
746
+ "padH": 1,
747
+ "padW": 1
748
+ },
749
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
750
+ "inputs": {
751
+ "x": { "shape": [1, 32, 112, 112], "dtype": "float32", "dist": "normal", "seed": 895, "scale": 0.2 },
752
+ "w": { "shape": [64, 32, 3, 3], "dtype": "float32", "dist": "normal", "seed": 3583, "scale": 0.05 }
753
+ },
754
+ "outputs": { "y": { "shape": [1, 64, 112, 112], "dtype": "float32" } },
755
+ "bench": {
756
+ "metrics": [
757
+ {
758
+ "type": "gflops",
759
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
760
+ }
761
+ ]
762
+ }
763
+ },
764
+ {
765
+ "name": "fusedconv-1x1-oddM66-c64-32x32",
766
+ "preset": "smoke",
767
+ "vars": {
768
+ "batch": 1,
769
+ "inChannels": 64,
770
+ "outChannels": 66,
771
+ "inH": 32,
772
+ "inW": 32,
773
+ "kernelH": 1,
774
+ "kernelW": 1,
775
+ "strideH": 1,
776
+ "strideW": 1,
777
+ "padH": 0,
778
+ "padW": 0
779
+ },
780
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
781
+ "inputs": {
782
+ "x": { "shape": [1, 64, 32, 32], "dtype": "float32", "dist": "normal", "seed": 7607, "scale": 0.2 },
783
+ "w": { "shape": [66, 64, 1, 1], "dtype": "float32", "dist": "normal", "seed": 4643, "scale": 0.05 }
784
+ },
785
+ "outputs": { "y": { "shape": [1, 66, 32, 32], "dtype": "float32" } },
786
+ "bench": {
787
+ "metrics": [
788
+ {
789
+ "type": "gflops",
790
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
791
+ }
792
+ ]
793
+ }
794
+ },
795
+ {
796
+ "name": "fusedconv-1x1-pow2M64-c64-32x32",
797
+ "preset": "smoke",
798
+ "vars": {
799
+ "batch": 1,
800
+ "inChannels": 64,
801
+ "outChannels": 64,
802
+ "inH": 32,
803
+ "inW": 32,
804
+ "kernelH": 1,
805
+ "kernelW": 1,
806
+ "strideH": 1,
807
+ "strideW": 1,
808
+ "padH": 0,
809
+ "padW": 0
810
+ },
811
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
812
+ "inputs": {
813
+ "x": { "shape": [1, 64, 32, 32], "dtype": "float32", "dist": "normal", "seed": 8186, "scale": 0.2 },
814
+ "w": { "shape": [64, 64, 1, 1], "dtype": "float32", "dist": "normal", "seed": 7554, "scale": 0.05 }
815
+ },
816
+ "outputs": { "y": { "shape": [1, 64, 32, 32], "dtype": "float32" } },
817
+ "bench": {
818
+ "metrics": [
819
+ {
820
+ "type": "gflops",
821
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
822
+ }
823
+ ]
824
+ }
825
+ },
826
+ {
827
+ "name": "fusedconv-group2-k7-c64m64-64x64-direct-fallback",
828
+ "provenance": {
829
+ "notes": "Named for the direct_unrolled route it was authored to expose. It now selects grouped_large_kernel_w4, the wide-output grouped kernel FusedConv gained from ai.onnx.Conv; kept under its original name as the grouped route regression guard."
830
+ },
831
+ "preset": "stress",
832
+ "vars": {
833
+ "batch": 1,
834
+ "inChannels": 64,
835
+ "outChannels": 64,
836
+ "inH": 64,
837
+ "inW": 64,
838
+ "kernelH": 7,
839
+ "kernelW": 7,
840
+ "strideH": 1,
841
+ "strideW": 1,
842
+ "padH": 3,
843
+ "padW": 3,
844
+ "group": 2
845
+ },
846
+ "attrs": { "group": 2, "activation": "Relu", "strides": [1, 1], "pads": [3, 3, 3, 3] },
847
+ "inputs": {
848
+ "x": { "shape": [1, 64, 64, 64], "dtype": "float32", "dist": "normal", "seed": 4801, "scale": 0.2 },
849
+ "w": { "shape": [64, 32, 7, 7], "dtype": "float32", "dist": "normal", "seed": 4802, "scale": 0.02 }
850
+ },
851
+ "outputs": { "y": { "shape": [1, 64, 64, 64], "dtype": "float32" } },
852
+ "bench": {
853
+ "metrics": [
854
+ {
855
+ "type": "gflops",
856
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
857
+ }
858
+ ]
859
+ }
860
+ },
861
+ {
862
+ "name": "fusedconv-group4-k5-c128m128-48x48-direct-fallback",
863
+ "provenance": {
864
+ "notes": "Named for the direct_unrolled route it was authored to expose. It now selects grouped_large_kernel_w4, and its 5x5 kernel also sits in the band that kernel refused before its kernel-size floor was measured."
865
+ },
866
+ "preset": "stress",
867
+ "vars": {
868
+ "batch": 1,
869
+ "inChannels": 128,
870
+ "outChannels": 128,
871
+ "inH": 48,
872
+ "inW": 48,
873
+ "kernelH": 5,
874
+ "kernelW": 5,
875
+ "strideH": 1,
876
+ "strideW": 1,
877
+ "padH": 2,
878
+ "padW": 2,
879
+ "group": 4
880
+ },
881
+ "attrs": { "group": 4, "activation": "Relu", "strides": [1, 1], "pads": [2, 2, 2, 2] },
882
+ "inputs": {
883
+ "x": { "shape": [1, 128, 48, 48], "dtype": "float32", "dist": "normal", "seed": 4811, "scale": 0.2 },
884
+ "w": { "shape": [128, 32, 5, 5], "dtype": "float32", "dist": "normal", "seed": 4812, "scale": 0.02 }
885
+ },
886
+ "outputs": { "y": { "shape": [1, 128, 48, 48], "dtype": "float32" } },
887
+ "bench": {
888
+ "metrics": [
889
+ {
890
+ "type": "gflops",
891
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
892
+ }
893
+ ]
894
+ }
895
+ },
896
+ {
897
+ "name": "fusedconv-conv1d-hardswish-b1c80m512-w3000-k3s1",
898
+ "preset": "stress",
899
+ "provenance": {
900
+ "source": "ONNX Runtime FusedConv provider semantics",
901
+ "notes": "Large aligned temporal convolution with bias and provider-backed HardSwish on the register-tiled implicit-GEMM path."
902
+ },
903
+ "vars": { "batch": 1, "inChannels": 80, "outChannels": 512, "inW": 3000, "kernelW": 3, "strideW": 1, "padW": 1 },
904
+ "attrs": { "activation": "HardSwish", "strides": [1], "pads": [1, 1] },
905
+ "inputs": {
906
+ "x": { "shape": [1, 80, 3000], "dtype": "float32", "dist": "normal", "seed": 481, "scale": 0.2 },
907
+ "w": { "shape": [512, 80, 3], "dtype": "float32", "dist": "normal", "seed": 482, "scale": 0.1 },
908
+ "bias": { "shape": [512], "dtype": "float32", "dist": "normal", "seed": 483, "scale": 0.05 }
909
+ },
910
+ "outputs": { "y": { "shape": [1, 512, 3000], "dtype": "float32" } },
911
+ "bench": {
912
+ "primary": true,
913
+ "metrics": [
914
+ {
915
+ "type": "gflops",
916
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * args.inChannels * args.kernelW"
917
+ }
918
+ ]
919
+ }
920
+ }
921
+ ]
922
+ }
build/webgpu/conv-1x1-gemm-tiled-reg.wgsl.jinja ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 1x1 / im2col Conv routed as a batched GEMM:
2
+ // Y[b] (M x N) = W (M x K) * X[b] (K x N), with M = outChannels,
3
+ // K = inChannels * kh * kw after im2col, and N = H * W. Register-blocked
4
+ // workgroup lanes compute TM x TN micro-tiles. The defaults use 16x16 lanes with
5
+ // an 8x4 micro-tile over a 64x64 output tile. Weight A is shared across the batch;
6
+ // input B and the output use the dispatch.z batch offset. Accumulation is f32;
7
+ // the optional epilogue applies bias, residual Z, and activation in the
8
+ // accumulator domain on store. Every dimension is bounds-checked.
9
+ // f16 operands remain packed in workgroup memory and widen only at the FMA site;
10
+ // the f32 accumulation order is retained while shared traffic is halved.
11
+ //
12
+ // Both shared tiles are stored as vec4 groups along the axis each lane consumes
13
+ // contiguously: tileA groups four K values for one output row, tileB groups four
14
+ // N columns for one K. One micro-tile step therefore reads TM + 4 vector words
15
+ // instead of 4 * (TM + TN) scalars, so a shared word feeds four times as many
16
+ // FMAs and the K loop runs four accumulation steps per iteration.
17
+ {% if usesF16 %}
18
+ enable f16;
19
+ {% endif %}
20
+ {{ env.wgsl.resourceDeclarations }}
21
+ {% set hasActivation = hasActivation is defined and hasActivation %}
22
+ {% if hasActivation %}
23
+ // Apply the fused activation in the f32 accumulator before the single output
24
+ // cast, avoiding an intermediate convolution tensor.
25
+ fn fused_act(v: f32) -> f32 {
26
+ {% if activation == "Relu" %}
27
+ return max(v, 0.0);
28
+ {% elif activation == "Clip" %}
29
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
30
+ {% elif activation == "LeakyRelu" %}
31
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
32
+ {% elif activation == "Sigmoid" %}
33
+ return 1.0 / (1.0 + exp(-v));
34
+ {% elif activation == "Tanh" %}
35
+ return tanh(v);
36
+ {% elif activation == "HardSigmoid" %}
37
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
38
+ {% else %}
39
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
40
+ {% endif %}
41
+ }
42
+ {% endif %}
43
+
44
+ {% set tileT = "f16" if usesF16 else "f32" -%}
45
+ {%- set GEMM_BK = gemmKTile if gemmKTile is defined else 16 %}
46
+ {% set GEMM_TM = gemmThreadRows if gemmThreadRows is defined else 8 %}
47
+ {% set GEMM_WG_X = gemmWorkgroupX if gemmWorkgroupX is defined else 16 %}
48
+ {% set GEMM_BM = gemmMTile if gemmMTile is defined else 64 %}
49
+ {% set GEMM_BN = gemmNTile if gemmNTile is defined else GEMM_WG_X * 4 %}
50
+ {% set GEMM_TN = gemmThreadColumns if gemmThreadColumns is defined else (GEMM_BN / GEMM_WG_X)|int %}
51
+ {% set GEMM_WG_Y = gemmWorkgroupY if gemmWorkgroupY is defined else (GEMM_BM / GEMM_TM)|int %}
52
+ {% set components = ["x", "y", "z", "w"] %}
53
+ const BK: u32 = {{ GEMM_BK }}u;
54
+ const BM: u32 = {{ GEMM_BM }}u;
55
+ const BN: u32 = {{ GEMM_BN }}u;
56
+ const TM: u32 = {{ GEMM_TM }}u;
57
+ const TN: u32 = {{ GEMM_TN }}u;
58
+ const WG_X: u32 = {{ GEMM_WG_X }}u;
59
+ const WG_SIZE: u32 = {{ GEMM_WG_X * GEMM_WG_Y }}u;
60
+ // Vector words per tile row: A groups K, B groups N.
61
+ const AK_VECS: u32 = BK / 4u;
62
+ const BN_VECS: u32 = BN / 4u;
63
+
64
+ // implicitIm2col leaves B unmaterialized. While staging a tile, the kernel
65
+ // decodes each (k, n) column-matrix coordinate into a raw NCHW input address.
66
+ // Overlapping windows may reread input values, trading address arithmetic and
67
+ // cache traffic for the storage and bandwidth of the expanded column matrix.
68
+ {% set implicitIm2col = implicitIm2col is defined and implicitIm2col %}
69
+ {% set fusedNarrowProjection = fusedNarrowProjection is defined and fusedNarrowProjection %}
70
+ {% if fusedNarrowProjection %}
71
+ {% set projectionChannels = projectionOutChannels %}
72
+ {% set projectionInputAct = inputActivation %}
73
+ {% set projectionOutputAct = outputActivation %}
74
+ {% set projectionScaled = hasOutputScale %}
75
+ {% set projectionBiased = hasProjectionBias %}
76
+ {% endif %}
77
+ {% set narrowProjectionTile = "tileB" if GEMM_BK >= GEMM_BM else "projectionTile" %}
78
+ {% set splitKValue = splitK if splitK is defined else 1 %}
79
+ {% set splitKPartial = splitKValue > 1 %}
80
+ {% set gemmKLimit = "kEnd" if splitKPartial else "K" %}
81
+ {% set implicitGatherMode = tunables.IMPLICIT_TILED_GATHER_MODE if tunables.IMPLICIT_TILED_GATHER_MODE is defined else 0 %}
82
+ {% set implicitGatherWgSize = GEMM_WG_X * GEMM_WG_Y %}
83
+ {% set implicitGatherColumnVecs = (GEMM_BN / 4)|int %}
84
+ {% set implicitGatherKChunks = (implicitGatherWgSize / implicitGatherColumnVecs)|int %}
85
+ {% set implicitGatherDivides = implicitGatherWgSize % implicitGatherColumnVecs == 0 and implicitGatherKChunks > 0 and GEMM_BK % implicitGatherKChunks == 0 %}
86
+ {% set useCarriedImplicitGather = implicitIm2col and implicitGatherMode == 1 and implicitGatherDivides %}
87
+ {% if useCarriedImplicitGather %}
88
+ {% set implicitGatherLoadWidth = (GEMM_BK / implicitGatherKChunks)|int %}
89
+ {% endif %}
90
+ {% if implicitIm2col %}
91
+ const CONV_KERNEL_H: u32 = {{ convKernelH }}u;
92
+ const CONV_KERNEL_W: u32 = {{ convKernelW }}u;
93
+ const CONV_KSIZE: u32 = CONV_KERNEL_H * CONV_KERNEL_W;
94
+ const CONV_STRIDE_H: u32 = {{ convStrideH }}u;
95
+ const CONV_STRIDE_W: u32 = {{ convStrideW }}u;
96
+ const CONV_DILATION_H: u32 = {{ convDilationH }}u;
97
+ const CONV_DILATION_W: u32 = {{ convDilationW }}u;
98
+ const CONV_PAD_TOP: i32 = {{ convPadTop }};
99
+ const CONV_PAD_LEFT: i32 = {{ convPadLeft }};
100
+ const CONV_IN_H: u32 = {{ convInH }}u;
101
+ const CONV_IN_W: u32 = {{ convInW }}u;
102
+ const CONV_OUT_W: u32 = {{ convOutW }}u;
103
+ const CONV_IN_CHANNELS: u32 = {{ convInChannels }}u;
104
+ {% endif %}
105
+
106
+ {% if fusedNarrowProjection %}
107
+
108
+ const PROJECTION_OUT_C: u32 = {{ projectionChannels }}u;
109
+ {% else %}{% set emitConvStoreOut = not splitKPartial %}{% if emitConvStoreOut | default(true) %}fn store_out(m: u32, n: u32, yBase: u32, raw: f32) {
110
+ if (m >= params.M || n >= params.N) {
111
+ return;
112
+ }
113
+ var v = raw;
114
+ {% if hasBias %}
115
+ v = v + f32(bias[m]);
116
+ {% endif %}
117
+ {% if hasZ %}
118
+ v = v + f32(zResidual[yBase + m * params.N + n]);
119
+ {% endif %}
120
+ {% if hasActivation %}
121
+ v = fused_act(v);
122
+ {% endif %}
123
+ y[yBase + m * params.N + n] = {{ T }}(v);
124
+ }
125
+ {% endif %}
126
+ {% endif %}
127
+
128
+ var<workgroup> tileA: array<array<vec4<{{ tileT }}>, AK_VECS>, BM>;
129
+ var<workgroup> tileB: array<array<vec4<{{ tileT }}>, BN_VECS>, BK>;
130
+ {% if fusedNarrowProjection and GEMM_BK < GEMM_BM %}
131
+ // BK16 cannot reuse the 16-row input tile to publish a BM32 intermediate.
132
+ // The extra 32x64 f32 tile keeps the total at 14 KiB, below WebGPU's
133
+ // guaranteed 16 KiB workgroup-storage floor.
134
+ var<workgroup> projectionTile: array<array<vec4<f32>, BN_VECS>, BM>;
135
+ {% endif %}
136
+ {% macro publish_projection_input(localRow, localVec, component, globalRow, globalColumn, raw) %}
137
+ if ({{ globalRow }} < M && {{ globalColumn }} < N) {
138
+ var projectionInput = {{ raw }};
139
+ {% if hasBias %}
140
+ projectionInput = projectionInput + f32(bias[{{ globalRow }}]);
141
+ {% endif %}
142
+ {% if projectionInputAct == "relu" %}
143
+ projectionInput = projection_relu(projectionInput);
144
+ {% endif %}
145
+ {{ narrowProjectionTile }}[{{ localRow }}][{{ localVec }}].{{ component }} = projectionInput;
146
+ } else {
147
+ {{ narrowProjectionTile }}[{{ localRow }}][{{ localVec }}].{{ component }} = 0.0;
148
+ }
149
+ {%- endmacro %}
150
+ {% macro load_a_vec4(rowBase, columnBase, rowLimit, columnLimit) %}
151
+ for (var linear = li; linear < BM * AK_VECS; linear += WG_SIZE) {
152
+ let ar = linear / AK_VECS;
153
+ let ac4 = linear % AK_VECS;
154
+ let am = {{ rowBase }} + ar;
155
+ let ak = {{ columnBase }} + ac4 * 4u;
156
+ var av = vec4<{{ tileT }}>({{ tileT }}(0.0));
157
+ if (am < {{ rowLimit }}) {
158
+ let aBase = am * K + ak;
159
+ if (ak + 3u < {{ columnLimit }}) {
160
+ av = vec4<{{ tileT }}>({{ tileT }}(w[aBase]), {{ tileT }}(w[aBase + 1u]), {{ tileT }}(w[aBase + 2u]), {{ tileT }}(w[aBase + 3u]));
161
+ } else {
162
+ for (var t = 0u; t < 4u; t = t + 1u) {
163
+ if (ak + t < {{ columnLimit }}) {
164
+ av[t] = {{ tileT }}(w[aBase + t]);
165
+ }
166
+ }
167
+ }
168
+ }
169
+ tileA[ar][ac4] = av;
170
+ }
171
+ {%- endmacro %}
172
+ {% macro load_b_vec4(rowBase, columnBase, rowLimit, columnLimit) %}
173
+ for (var linear = li; linear < BK * BN_VECS; linear += WG_SIZE) {
174
+ let br = linear / BN_VECS;
175
+ let bc4 = linear % BN_VECS;
176
+ let bk = {{ rowBase }} + br;
177
+ let bn = {{ columnBase }} + bc4 * 4u;
178
+ var bvec = vec4<{{ tileT }}>({{ tileT }}(0.0));
179
+ if (bk < {{ rowLimit }}) {
180
+ let bBase = xBatchBase + bk * N + bn;
181
+ if (bn + 3u < {{ columnLimit }}) {
182
+ bvec = vec4<{{ tileT }}>({{ tileT }}(xm[bBase]), {{ tileT }}(xm[bBase + 1u]), {{ tileT }}(xm[bBase + 2u]), {{ tileT }}(xm[bBase + 3u]));
183
+ } else {
184
+ for (var t = 0u; t < 4u; t = t + 1u) {
185
+ if (bn + t < {{ columnLimit }}) {
186
+ bvec[t] = {{ tileT }}(xm[bBase + t]);
187
+ }
188
+ }
189
+ }
190
+ }
191
+ tileB[br][bc4] = bvec;
192
+ }
193
+ {%- endmacro %}
194
+ {% macro load_b_implicit_vec4(rowBase, columnBase, rowLimit, columnLimit) %}
195
+ for (var linear = li; linear < BK * BN_VECS; linear += WG_SIZE) {
196
+ let br = linear / BN_VECS;
197
+ let bc4 = linear % BN_VECS;
198
+ let bk = {{ rowBase }} + br;
199
+ var bvec = vec4<{{ tileT }}>({{ tileT }}(0.0));
200
+ if (bk < {{ rowLimit }}) {
201
+ // k is the OIHW weight flattening (ic * KH + kh) * KW + kw; n is the output position.
202
+ let ic = bk / CONV_KSIZE;
203
+ let kq = bk % CONV_KSIZE;
204
+ let kh = kq / CONV_KERNEL_W;
205
+ let kw = kq % CONV_KERNEL_W;
206
+ let planeBase = (batch * CONV_IN_CHANNELS + ic) * CONV_IN_H;
207
+ for (var t = 0u; t < 4u; t = t + 1u) {
208
+ let bn = {{ columnBase }} + bc4 * 4u + t;
209
+ if (bn < {{ columnLimit }}) {
210
+ let oh = bn / CONV_OUT_W;
211
+ let ow = bn % CONV_OUT_W;
212
+ let ih = i32(oh * CONV_STRIDE_H + kh * CONV_DILATION_H) - CONV_PAD_TOP;
213
+ let iw = i32(ow * CONV_STRIDE_W + kw * CONV_DILATION_W) - CONV_PAD_LEFT;
214
+ if (ih >= 0 && ih < i32(CONV_IN_H) && iw >= 0 && iw < i32(CONV_IN_W)) {
215
+ bvec[t] = {{ tileT }}(xm[(planeBase + u32(ih)) * CONV_IN_W + u32(iw)]);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ tileB[br][bc4] = bvec;
221
+ }
222
+ {%- endmacro %}
223
+ {% macro load_b_implicit_carried(rowBase, columnBase, rowLimit, columnLimit) %}
224
+ let bColVec = li / {{ implicitGatherKChunks }}u;
225
+ let bChunk = li % {{ implicitGatherKChunks }}u;
226
+ let bLocalK = bChunk * {{ implicitGatherLoadWidth }}u;
227
+ let bStartK = {{ rowBase }} + bLocalK;
228
+ let bCol0 = {{ columnBase }} + bColVec * 4u;
229
+ let bCols = vec4<u32>(bCol0, bCol0 + 1u, bCol0 + 2u, bCol0 + 3u);
230
+ let inColumn = bCols < vec4<u32>({{ columnLimit }});
231
+ let ih0 = vec4<i32>((bCols / vec4<u32>(CONV_OUT_W)) * CONV_STRIDE_H) - vec4<i32>(CONV_PAD_TOP);
232
+ let iw0 = vec4<i32>((bCols % vec4<u32>(CONV_OUT_W)) * CONV_STRIDE_W) - vec4<i32>(CONV_PAD_LEFT);
233
+ var carriedK = bStartK;
234
+ var carriedIc = carriedK / CONV_KSIZE;
235
+ let carriedKq = carriedK % CONV_KSIZE;
236
+ var carriedKh = carriedKq / CONV_KERNEL_W;
237
+ var carriedKw = carriedKq % CONV_KERNEL_W;
238
+ var carriedIh = ih0 + vec4<i32>(i32(carriedKh * CONV_DILATION_H));
239
+ var carriedIw = iw0 + vec4<i32>(i32(carriedKw * CONV_DILATION_W));
240
+
241
+ // Interior words can carry four addresses. Padding, the N tail, and the
242
+ // final K tile retain the coordinate-checked loop below and publish exact
243
+ // zeros.
244
+ let interior = all(inColumn)
245
+ && bStartK + {{ implicitGatherLoadWidth }}u <= {{ rowLimit }}
246
+ && all(ih0 >= vec4<i32>(0))
247
+ && all(ih0 + vec4<i32>(i32((CONV_KERNEL_H - 1u) * CONV_DILATION_H)) < vec4<i32>(i32(CONV_IN_H)))
248
+ && all(iw0 >= vec4<i32>(0))
249
+ && all(iw0 + vec4<i32>(i32((CONV_KERNEL_W - 1u) * CONV_DILATION_W)) < vec4<i32>(i32(CONV_IN_W)));
250
+ if (interior) {
251
+ let batchBase = batch * CONV_IN_CHANNELS * CONV_IN_H * CONV_IN_W;
252
+ let colStep = i32(CONV_DILATION_W);
253
+ let rowStep = i32(CONV_DILATION_H * CONV_IN_W);
254
+ let planeStep = i32(CONV_IN_H * CONV_IN_W);
255
+ let kwWrap = colStep * i32(CONV_KERNEL_W) - rowStep;
256
+ let khWrap = rowStep * i32(CONV_KERNEL_H) - planeStep;
257
+ var carriedAddress = vec4<i32>(i32(carriedIc) * planeStep) + carriedIh * vec4<i32>(i32(CONV_IN_W)) + carriedIw;
258
+ for (var i = 0u; i < {{ implicitGatherLoadWidth }}u; i = i + 1u) {
259
+ tileB[bLocalK + i][bColVec] = vec4<{{ tileT }}>(
260
+ {% for component in components %}
261
+ {{ tileT }}(xm[batchBase + u32(carriedAddress.{{ component }})]){% if not loop.last %},{% endif %}
262
+ {% endfor %}
263
+ );
264
+ carriedAddress = carriedAddress + vec4<i32>(colStep);
265
+ carriedKw = carriedKw + 1u;
266
+ if (carriedKw == CONV_KERNEL_W) {
267
+ carriedKw = 0u;
268
+ carriedAddress = carriedAddress - vec4<i32>(kwWrap);
269
+ carriedKh = carriedKh + 1u;
270
+ if (carriedKh == CONV_KERNEL_H) {
271
+ carriedKh = 0u;
272
+ carriedAddress = carriedAddress - vec4<i32>(khWrap);
273
+ }
274
+ }
275
+ }
276
+ } else {
277
+ for (var i = 0u; i < {{ implicitGatherLoadWidth }}u; i = i + 1u) {
278
+ var carriedValue = vec4<{{ tileT }}>({{ tileT }}(0.0));
279
+ let carriedRow = (batch * CONV_IN_CHANNELS + carriedIc) * CONV_IN_H;
280
+ let carriedOk = inColumn
281
+ & (carriedIh >= vec4<i32>(0)) & (carriedIh < vec4<i32>(i32(CONV_IN_H)))
282
+ & (carriedIw >= vec4<i32>(0)) & (carriedIw < vec4<i32>(i32(CONV_IN_W)));
283
+ if (carriedK < {{ rowLimit }}) {
284
+ {% for component in components %}
285
+ if (carriedOk.{{ component }}) {
286
+ carriedValue.{{ component }} = {{ tileT }}(xm[(carriedRow + u32(carriedIh.{{ component }})) * CONV_IN_W + u32(carriedIw.{{ component }})]);
287
+ }
288
+ {% endfor %}
289
+ }
290
+ tileB[bLocalK + i][bColVec] = carriedValue;
291
+ carriedK = carriedK + 1u;
292
+ carriedKw = carriedKw + 1u;
293
+ carriedIw = carriedIw + vec4<i32>(i32(CONV_DILATION_W));
294
+ if (carriedKw == CONV_KERNEL_W) {
295
+ carriedKw = 0u;
296
+ carriedIw = iw0;
297
+ carriedKh = carriedKh + 1u;
298
+ carriedIh = carriedIh + vec4<i32>(i32(CONV_DILATION_H));
299
+ if (carriedKh == CONV_KERNEL_H) {
300
+ carriedKh = 0u;
301
+ carriedIh = ih0;
302
+ carriedIc = carriedIc + 1u;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ {%- endmacro %}
308
+
309
+ @compute @workgroup_size({{ GEMM_WG_X }}, {{ GEMM_WG_Y }}, 1)
310
+ fn main(
311
+ @builtin(workgroup_id) wg: vec3<u32>,
312
+ @builtin(local_invocation_id) lid: vec3<u32>
313
+ ) {
314
+ let M = params.M;
315
+ let K = params.K;
316
+ let N = params.N;
317
+ let mBase = wg.y * BM;
318
+ let nBase = wg.x * BN;
319
+ let batch = wg.z;
320
+ {% if not implicitIm2col %}
321
+ let xBatchBase = batch * K * N;
322
+ {% endif %}
323
+ let li = lid.y * WG_X + lid.x;
324
+
325
+ // Explicit vectors give the backend fixed register indices and expose TM
326
+ // independent FMA chains.
327
+ {% for row in range(GEMM_TM) %}
328
+ var acc{{ row }} = vec{{ GEMM_TN }}<f32>(0.0);
329
+ {% endfor %}
330
+
331
+ let numTiles = (K + BK - 1u) / BK;
332
+ for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
333
+ let kBase = kt * BK;
334
+ {{ load_a_vec4("mBase", "kBase", "M", gemmKLimit) }}
335
+ {% if useCarriedImplicitGather %}
336
+ {{ load_b_implicit_carried("kBase", "nBase", gemmKLimit, "N") }}
337
+ {% elif implicitIm2col %}
338
+ {{ load_b_implicit_vec4("kBase", "nBase", gemmKLimit, "N") }}
339
+ {% else %}
340
+ {{ load_b_vec4("kBase", "nBase", gemmKLimit, "N") }}
341
+ {% endif %}
342
+ workgroupBarrier();
343
+ // One iteration consumes four K steps: TM vector words of A and four of B
344
+ // cover TM x TN x 4 FMAs.
345
+ let aRow = lid.y * TM;
346
+ for (var kv: u32 = 0u; kv < AK_VECS; kv = kv + 1u) {
347
+ {% for row in range(GEMM_TM) %}
348
+ let av{{ row }} = vec4<f32>(tileA[aRow + {{ row }}u][kv]);
349
+ {% endfor %}
350
+ {% for sub in range(4) %}
351
+ let bv{{ sub }} = vec{{ GEMM_TN }}<f32>(tileB[kv * 4u + {{ sub }}u][lid.x]);
352
+ {% for row in range(GEMM_TM) %}
353
+ acc{{ row }} += vec{{ GEMM_TN }}<f32>(av{{ row }}.{{ components[sub] }}) * bv{{ sub }};
354
+ {% endfor %}
355
+ {% endfor %}
356
+ }
357
+ workgroupBarrier();
358
+ }
359
+
360
+ {% if not fusedNarrowProjection %}
361
+ let yBatchBase = batch * M * N;
362
+ {% endif %}
363
+ let m0 = mBase + lid.y * TM;
364
+ let n0 = nBase + lid.x * TN;
365
+ {% for row in range(GEMM_TM) %}
366
+ {% for column in range(GEMM_TN) %}
367
+ {% if fusedNarrowProjection %}
368
+ {{ publish_projection_input("lid.y * TM + " ~ row ~ "u", "lid.x * " ~ ((GEMM_TN / 4)|int) ~ "u + " ~ ((column / 4)|int) ~ "u", components[column % 4], "m0 + " ~ row ~ "u", "n0 + " ~ column ~ "u", "acc" ~ row ~ "." ~ components[column]) }}
369
+ {% else %}
370
+ store_out(m0 + {{ row }}u, n0 + {{ column }}u, yBatchBase, acc{{ row }}.{{ components[column] }});
371
+ {% endif %}
372
+ {% endfor %}
373
+ {% endfor %}{% if fusedNarrowProjection %}
374
+
375
+ // Conv's final K-tile barrier makes tileB dead before it becomes the
376
+ // intermediate tile. Every lane publishes its unique micro-tile, then one
377
+ // lane per spatial vector word consumes all logical M rows in increasing
378
+ // order, four adjacent columns at a time. Invalid M/N-tail cells are
379
+ // initialized above, so the barrier is uniform and no lane can observe a
380
+ // stale input-tile value.
381
+ workgroupBarrier();
382
+ if (li < BN_VECS) {
383
+ let projectionNBase = nBase + li * 4u;
384
+ {% for oc in range(projectionChannels) %}
385
+ var projectionAcc{{ oc }} = vec4<f32>(0.0);
386
+ {% endfor %}
387
+ for (var channel = 0u; channel < M; channel = channel + 1u) {
388
+ let projectionValues = vec4<f32>({{ narrowProjectionTile }}[channel][li]);
389
+ {% for oc in range(projectionChannels) %}
390
+ projectionAcc{{ oc }} = projectionAcc{{ oc }} + projectionValues * f32(projectionW[{{ oc }}u * M + channel]);
391
+ {% endfor %}
392
+ }
393
+
394
+ let projectionYBase = batch * PROJECTION_OUT_C * N;
395
+ {% for oc in range(projectionChannels) %}
396
+ let projected{{ oc }} = projectionAcc{{ oc }}{% if projectionBiased %} + vec4<f32>(f32(projectionBias[{{ oc }}u])){% endif %};
397
+ {% endfor %}
398
+ {% for column in range(4) %}
399
+ let projectionN{{ column }} = projectionNBase + {{ column }}u;
400
+ if (projectionN{{ column }} < N) {
401
+ {% for oc in range(projectionChannels) %}
402
+ {% if projectionOutputAct == "relu" %}
403
+ let activated{{ oc }}_{{ column }} = projection_relu(projected{{ oc }}.{{ components[column] }});
404
+ {% elif projectionOutputAct == "sigmoid" %}
405
+ let activated{{ oc }}_{{ column }} = sigmoid_safe(projected{{ oc }}.{{ components[column] }});
406
+ {% else %}
407
+ let activated{{ oc }}_{{ column }} = projected{{ oc }}.{{ components[column] }};
408
+ {% endif %}
409
+ {% if projectionScaled %}
410
+ y[projectionYBase + {{ oc }}u * N + projectionN{{ column }}] = activated{{ oc }}_{{ column }} * params.outputScale;
411
+ {% else %}
412
+ y[projectionYBase + {{ oc }}u * N + projectionN{{ column }}] = activated{{ oc }}_{{ column }};
413
+ {% endif %}
414
+ {% endfor %}
415
+ }
416
+ {% endfor %}
417
+ }
418
+ {% endif %}
419
+ }
build/webgpu/conv-1x1-gemm-tiled.wgsl.jinja ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 1x1 Conv routed as a batched GEMM: Y[b] (M x N) = W (M x K) * X[b] (K x N)
2
+ // with M = outChannels, K = inChannels, N = H*W. For a 1x1 kernel with
3
+ // group == 1, stride 1, no padding, and dilation 1, the NCHW input is already
4
+ // the [K, N] matrix and the OIHW weight is already the [M, K] matrix — no
5
+ // data movement, only index arithmetic.
6
+ //
7
+ // Register-blocked 32x32 shared-memory tile (16x16 threads, each computes a 2x2
8
+ // micro-tile, with a selectable K tile (16 by default), the same structure as
9
+ // the general tiled matmul path — each shared element feeds 2 FMAs
10
+ // and a 32-wide column tile is staged once instead of re-reading W per output
11
+ // column. The weight matrix (A) is shared across the batch (no batch stride);
12
+ // the input (B) and output carry the batch offset via dispatch.z. f32
13
+ // accumulation, cast to the output element type on store; the optional bias is
14
+ // per output channel (M row), added after the full K accumulation per ONNX Conv
15
+ // semantics. f16 operands stay packed in workgroup memory and widen only when
16
+ // consumed. Fully bounds-checked (any M/N/K).
17
+ {% if usesF16 %}
18
+ enable f16;
19
+ {% endif %}
20
+ {{ env.wgsl.resourceDeclarations }}
21
+ {% set hasActivation = hasActivation is defined and hasActivation %}
22
+ {% if hasActivation %}
23
+ // Apply the fused activation in the f32 accumulator before the single output
24
+ // cast, avoiding an intermediate convolution tensor.
25
+ fn fused_act(v: f32) -> f32 {
26
+ {% if activation == "Relu" %}
27
+ return max(v, 0.0);
28
+ {% elif activation == "Clip" %}
29
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
30
+ {% elif activation == "LeakyRelu" %}
31
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
32
+ {% elif activation == "Sigmoid" %}
33
+ return 1.0 / (1.0 + exp(-v));
34
+ {% elif activation == "Tanh" %}
35
+ return tanh(v);
36
+ {% elif activation == "HardSigmoid" %}
37
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
38
+ {% else %}
39
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
40
+ {% endif %}
41
+ }
42
+ {% endif %}
43
+
44
+ {% set tileT = "f16" if usesF16 else "f32" -%}
45
+ const BK: u32 = 16u;
46
+ const BM: u32 = 32u;
47
+ const BN: u32 = 32u;
48
+
49
+ // Store one output element with the optional bias/residual/activation epilogue in the
50
+ // f32 accumulator domain: Y = activation(conv + bias + Z).
51
+ fn store_out(m: u32, n: u32, yBase: u32, raw: f32) {
52
+ if (m >= params.M || n >= params.N) {
53
+ return;
54
+ }
55
+ var v = raw;
56
+ {% if hasBias %}
57
+ v = v + f32(bias[m]);
58
+ {% endif %}
59
+ {% if hasZ %}
60
+ v = v + f32(zResidual[yBase + m * params.N + n]);
61
+ {% endif %}
62
+ {% if hasActivation %}
63
+ v = fused_act(v);
64
+ {% endif %}
65
+ y[yBase + m * params.N + n] = {{ T }}(v);
66
+ }
67
+
68
+
69
+ var<workgroup> tileA: array<array<{{ tileT }}, BK>, BM>;
70
+ var<workgroup> tileB: array<array<{{ tileT }}, BN>, BK>;
71
+
72
+ @compute @workgroup_size(16, 16, 1)
73
+ fn main(
74
+ @builtin(workgroup_id) wg: vec3<u32>,
75
+ @builtin(local_invocation_id) lid: vec3<u32>
76
+ ) {
77
+ let M = params.M;
78
+ let K = params.K;
79
+ let N = params.N;
80
+ let mBase = wg.y * BM;
81
+ let nBase = wg.x * BN;
82
+ let batch = wg.z;
83
+ let xBatchBase = batch * K * N;
84
+ let li = lid.y * 16u + lid.x;
85
+
86
+ var acc00: f32 = 0.0;
87
+ var acc01: f32 = 0.0;
88
+ var acc10: f32 = 0.0;
89
+ var acc11: f32 = 0.0;
90
+ let numTiles = (K + BK - 1u) / BK;
91
+ for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
92
+ let kBase = kt * BK;
93
+ // Cooperative load: 32x16 A(=W) tile + 16x32 B(=X[batch]) tile, 256 threads x 2 each.
94
+ for (var e: u32 = 0u; e < (BM * BK) / 256u; e = e + 1u) {
95
+ let idx = li + e * 256u;
96
+ let ar = idx / BK;
97
+ let ac = idx % BK;
98
+ let am = mBase + ar;
99
+ let ak = kBase + ac;
100
+ if (am < M && ak < K) {
101
+ tileA[ar][ac] = {{ tileT }}(w[am * K + ak]);
102
+ } else {
103
+ tileA[ar][ac] = {{ tileT }}(0.0);
104
+ }
105
+ let br = idx / BN;
106
+ let bc = idx % BN;
107
+ let bk = kBase + br;
108
+ let bn = nBase + bc;
109
+ if (bk < K && bn < N) {
110
+ tileB[br][bc] = {{ tileT }}(xm[xBatchBase + bk * N + bn]);
111
+ } else {
112
+ tileB[br][bc] = {{ tileT }}(0.0);
113
+ }
114
+ }
115
+ workgroupBarrier();
116
+ for (var kk: u32 = 0u; kk < BK; kk = kk + 1u) {
117
+ let a0 = f32(tileA[lid.y * 2u][kk]);
118
+ let a1 = f32(tileA[lid.y * 2u + 1u][kk]);
119
+ let b0 = f32(tileB[kk][lid.x * 2u]);
120
+ let b1 = f32(tileB[kk][lid.x * 2u + 1u]);
121
+ acc00 = acc00 + a0 * b0;
122
+ acc01 = acc01 + a0 * b1;
123
+ acc10 = acc10 + a1 * b0;
124
+ acc11 = acc11 + a1 * b1;
125
+ }
126
+ workgroupBarrier();
127
+ }
128
+
129
+ let m0 = mBase + lid.y * 2u;
130
+ let m1 = m0 + 1u;
131
+ let n0 = nBase + lid.x * 2u;
132
+ let n1 = n0 + 1u;
133
+ let yBatchBase = batch * M * N;
134
+ store_out(m0, n0, yBatchBase, acc00);
135
+ store_out(m0, n1, yBatchBase, acc01);
136
+ store_out(m1, n0, yBatchBase, acc10);
137
+ store_out(m1, n1, yBatchBase, acc11);
138
+ }
build/webgpu/conv-1x1-subgroup-matrix.wgsl.jinja ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Convolution as subgroup-matrix GEMM. OIHW weights form row-major [M,K]; `xm`
2
+ // is either an NCHW 1x1 view [K,N] or a materialized im2col matrix. Bias is
3
+ // applied per output channel after the full f32 accumulation.
4
+ //
5
+ // Full 8x8 matrix loads require complete K/N tiles. The aligned path enforces
6
+ // that geometry; `padded` zero-fills tails, and `implicitIm2col` gathers and
7
+ // zero-fills directly from NCHW without materializing the column matrix.
8
+ //
9
+ // `splitK` assigns disjoint K ranges to workgroup_id.z. Each slice writes raw
10
+ // sums to tile-padded [splitK,batch,M_PAD,N_PAD] scratch; a later pass combines
11
+ // the slices and applies bias or an epilogue. Padded tail cells remain zero and
12
+ // are never copied to the logical output.
13
+ {% set fusedNarrowProjection = fusedNarrowProjection if fusedNarrowProjection is defined else false %}
14
+ {% set polyphase = polyphaseConvTranspose is defined and polyphaseConvTranspose %}
15
+ {% set splitKValue = splitK if splitK is defined else 1 %}
16
+ {% set splitKPartial = splitKValue > 1 %}
17
+ {% set kLoopVar = "K_LOOP" if padded else "K" %}
18
+ {% set nColsVar = "N_COLS" if padded else "N" %}
19
+ {% if usesF16 %}
20
+ enable f16;
21
+ {% endif %}
22
+ enable subgroups;
23
+ {% if pinSubgroupSize32 %}
24
+ enable subgroup_size_control;
25
+ {% endif %}
26
+ enable chromium_experimental_subgroup_matrix;
27
+ diagnostic(off, chromium.subgroup_matrix_uniformity);
28
+
29
+ {{ env.wgsl.resourceDeclarations }}
30
+
31
+ {% set operandScalar = fScalar %}
32
+ {% set accScalar = "f32" %}
33
+ {% set useDirectMatrixStore = directMatrixStore is defined and directMatrixStore %}
34
+ {% set tileRowsValue = tileRows if tileRows is defined else 32 %}
35
+ {% set tileColsValue = tileCols if tileCols is defined else 64 %}
36
+ {% set workgroupThreadsValue = workgroupThreads if workgroupThreads is defined else 128 %}
37
+ {% set subgroupRowsValue = subgroupRows if subgroupRows is defined else 2 %}
38
+ {% set subgroupColsValue = subgroupCols if subgroupCols is defined else 2 %}
39
+ {% set subRowsValue = (tileRowsValue / subgroupRowsValue)|int %}
40
+ {% set subColsValue = (tileColsValue / subgroupColsValue)|int %}
41
+ {% set bLoadWidth = (tileColsValue * 32 / workgroupThreadsValue)|int %}
42
+ {% set bKChunks = (32 / bLoadWidth)|int %}
43
+
44
+ const M: u32 = {{ M }}u;
45
+ const K: u32 = {{ K }}u;
46
+ const N: u32 = {{ N }}u;
47
+ {% if padded %}
48
+ const K_LOOP: u32 = {{ kPadded }}u;
49
+ {% endif %}
50
+ {% if padded %}
51
+ const N_COLS: u32 = {{ nPadded }}u;
52
+ {% endif %}
53
+ const B_BATCH_STRIDE: u32 = {{ kLoopVar }} * {{ nColsVar }};
54
+ {% if not splitKPartial and not polyphase and not fusedNarrowProjection %}
55
+ const C_BATCH_STRIDE: u32 = M * N;
56
+ {% endif %}
57
+ const TILE_COLS: u32 = {{ tileColsValue }}u;
58
+ const TILE_ROWS: u32 = {{ tileRowsValue }}u;
59
+ const TILE_K: u32 = 32u;
60
+ const SUB_COLS: u32 = {{ subColsValue }}u;
61
+ const SUB_ROWS: u32 = {{ subRowsValue }}u;
62
+
63
+ var<workgroup> tile_A: array<{{ operandScalar }}, {{ tileRowsValue }} * 32>;
64
+ var<workgroup> tile_B: array<{{ operandScalar }}, {{ tileColsValue }} * 32>;
65
+ {% if (not useDirectMatrixStore or padded) and not splitKPartial and not fusedNarrowProjection %}
66
+ var<workgroup> scratch: array<array<array<{{ accScalar }}, 64>, 4>, {{ (workgroupThreadsValue / 32)|int }}>;
67
+
68
+ {% endif %}
69
+ fn loadSHMA(tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
70
+ let a_global = tile_base + row;
71
+ let col = c_idx * 8u;
72
+ for (var col_offset = 0u; col_offset < 8u; col_offset = col_offset + 1u) {
73
+ let k = k_idx + col + col_offset;
74
+ if (a_global < M{% if padded %} && k < K{% endif %}) {
75
+ {% set A_PHASE = "phase_base + " if polyphase else "" %}
76
+ {% if operandScalar == "f16" %}
77
+ tile_A[row * TILE_K + col + col_offset] = f16(w[{{ A_PHASE }}a_global * K + k]);
78
+ {% else %}
79
+ tile_A[row * TILE_K + col + col_offset] = f32(w[{{ A_PHASE }}a_global * K + k]);
80
+ {% endif %}
81
+ } else {
82
+ {% if operandScalar == "f16" %}
83
+ tile_A[row * TILE_K + col + col_offset] = 0.0h;
84
+ {% else %}
85
+ tile_A[row * TILE_K + col + col_offset] = 0.0;
86
+ {% endif %}
87
+ }
88
+ }
89
+ }
90
+
91
+ fn loadSHMB(b_base: u32, tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
92
+ let b_col = tile_base + row;
93
+ let col = c_idx * {{ bLoadWidth }}u;
94
+ for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
95
+ let k = k_idx + col + i;
96
+ {% if operandScalar == "f16" %}
97
+ tile_B[row * TILE_K + col + i] = f16(xm[b_base + k * {{ nColsVar }} + b_col]);
98
+ {% else %}
99
+ tile_B[row * TILE_K + col + i] = f32(xm[b_base + k * {{ nColsVar }} + b_col]);
100
+ {% endif %}
101
+ }
102
+ }
103
+
104
+ {% set hasActivation = hasActivation is defined and hasActivation %}
105
+ {% if hasActivation %}
106
+ // Apply the fused activation in the f32 accumulator before the single output
107
+ // cast, avoiding an intermediate convolution tensor.
108
+ fn fused_act(v: f32) -> f32 {
109
+ {% if activation == "Relu" %}
110
+ return max(v, 0.0);
111
+ {% elif activation == "Clip" %}
112
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
113
+ {% elif activation == "LeakyRelu" %}
114
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
115
+ {% elif activation == "Sigmoid" %}
116
+ return 1.0 / (1.0 + exp(-v));
117
+ {% elif activation == "Tanh" %}
118
+ return tanh(v);
119
+ {% elif activation == "HardSigmoid" %}
120
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
121
+ {% else %}
122
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
123
+ {% endif %}
124
+ }
125
+ {% endif %}
126
+
127
+ {% set biasAdd = " + bv" if hasBias else "" %}
128
+ {% if hasActivation or hasZ %}
129
+ // Fused epilogue: Y = activation(conv + bias + Z), applied at the output store.
130
+ // `raw` is the f32 convolution-plus-bias accumulator; Z shares Y's NCHW layout.
131
+ fn epi(raw: f32{% if hasZ %}, yIndex: u32{% endif %}) -> {{ T }} {
132
+ var r = raw;
133
+ {% if hasZ %}
134
+ r = r + f32(zResidual[yIndex]);
135
+ {% endif %}
136
+ {% if hasActivation %}
137
+ r = fused_act(r);
138
+ {% endif %}
139
+ return {{ T }}(r);
140
+ }
141
+ {% endif %}
142
+ {% macro store_val(valExpr, idxExpr) %}
143
+ {% if hasActivation or hasZ %}epi({{ valExpr }}{% if hasZ %}, {{ idxExpr }}{% endif %})
144
+ {%- else %}{{ T }}({{ valExpr }})
145
+ {%- endif %}
146
+ {% endmacro %}
147
+
148
+ {% if (not useDirectMatrixStore or padded) and not splitKPartial and not fusedNarrowProjection %}
149
+ fn storeOutput(offset: u32, {% if hasBias or polyphase %}row_base: u32, {% endif %}row: u32, col: u32, src_slot: u32, row_limit: i32{% if padded %}, col_anchor: u32{% endif %}) {
150
+ if (row_limit > 0 && row < u32(row_limit)) {
151
+ let col2 = col + 1u;
152
+ {% if hasBias %}
153
+ let bv = {{ accScalar }}(bias[row_base + row]);
154
+ {% endif %}
155
+ {% if padded %}
156
+ // Padded GEMM: the last column tile runs past the logical N; only write
157
+ // columns inside the real output. Each subgroup-matrix column maps to a
158
+ // distinct y column (anchor + local), so the guards are independent.
159
+ if (col_anchor + col + 0u < N) { y[offset + row * N + col] = {{ store_val("scratch[src_slot][0][row * 8u + col]" ~ biasAdd, "offset + row * N + col") }}; }
160
+ if (col_anchor + col + 8u < N) { y[offset + row * N + col + 8u] = {{ store_val("scratch[src_slot][1][row * 8u + col]" ~ biasAdd, "offset + row * N + col + 8u") }}; }
161
+ if (col_anchor + col + 16u < N) { y[offset + row * N + col + 16u] = {{ store_val("scratch[src_slot][2][row * 8u + col]" ~ biasAdd, "offset + row * N + col + 16u") }}; }
162
+ if (col_anchor + col + 24u < N) { y[offset + row * N + col + 24u] = {{ store_val("scratch[src_slot][3][row * 8u + col]" ~ biasAdd, "offset + row * N + col + 24u") }}; }
163
+ if (col_anchor + col2 + 0u < N) { y[offset + row * N + col2] = {{ store_val("scratch[src_slot][0][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2") }}; }
164
+ if (col_anchor + col2 + 8u < N) { y[offset + row * N + col2 + 8u] = {{ store_val("scratch[src_slot][1][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2 + 8u") }}; }
165
+ if (col_anchor + col2 + 16u < N) { y[offset + row * N + col2 + 16u] = {{ store_val("scratch[src_slot][2][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2 + 16u") }}; }
166
+ if (col_anchor + col2 + 24u < N) { y[offset + row * N + col2 + 24u] = {{ store_val("scratch[src_slot][3][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2 + 24u") }}; }
167
+ {% else %}
168
+ y[offset + row * N + col] = {{ store_val("scratch[src_slot][0][row * 8u + col]" ~ biasAdd, "offset + row * N + col") }};
169
+ y[offset + row * N + col + 8u] = {{ store_val("scratch[src_slot][1][row * 8u + col]" ~ biasAdd, "offset + row * N + col + 8u") }};
170
+ y[offset + row * N + col + 16u] = {{ store_val("scratch[src_slot][2][row * 8u + col]" ~ biasAdd, "offset + row * N + col + 16u") }};
171
+ y[offset + row * N + col + 24u] = {{ store_val("scratch[src_slot][3][row * 8u + col]" ~ biasAdd, "offset + row * N + col + 24u") }};
172
+
173
+ y[offset + row * N + col2] = {{ store_val("scratch[src_slot][0][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2") }};
174
+ y[offset + row * N + col2 + 8u] = {{ store_val("scratch[src_slot][1][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2 + 8u") }};
175
+ y[offset + row * N + col2 + 16u] = {{ store_val("scratch[src_slot][2][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2 + 16u") }};
176
+ y[offset + row * N + col2 + 24u] = {{ store_val("scratch[src_slot][3][row * 8u + col2]" ~ biasAdd, "offset + row * N + col2 + 24u") }};
177
+ {% endif %}
178
+ }
179
+ }
180
+
181
+ {% endif %}
182
+ @compute @workgroup_size({{ workgroupThreadsValue }}, 1, 1){{ " @subgroup_size(32)" if pinSubgroupSize32 else "" }}
183
+ fn main(
184
+ @builtin(workgroup_id) workgroup_id: vec3<u32>,
185
+ @builtin(local_invocation_index) local_idx: u32,
186
+ {% if (not useDirectMatrixStore or padded) and not splitKPartial and not fusedNarrowProjection %}
187
+ @builtin(subgroup_invocation_id) sg_id: u32,
188
+ {% endif %}
189
+ @builtin(subgroup_size) sg_size: u32
190
+ ) {
191
+ let batch = workgroup_id.z;
192
+ let b_base = batch * B_BATCH_STRIDE;
193
+ {% if not fusedNarrowProjection %}
194
+ let c_base = batch * C_BATCH_STRIDE;
195
+ {% endif %}
196
+ let a_global_base = workgroup_id.y * TILE_ROWS;
197
+ let b_global_base = workgroup_id.x * TILE_COLS;
198
+ let subtile_id = local_idx / sg_size;
199
+ let subtile_idx = subtile_id / {{ subgroupRowsValue }}u;
200
+ let subtile_idy = subtile_id % {{ subgroupRowsValue }}u;
201
+ let base_A = subtile_idy * SUB_ROWS;
202
+ let base_B = subtile_idx * SUB_COLS;
203
+
204
+ var matC00: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
205
+ var matC01: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
206
+ var matC02: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
207
+ var matC03: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
208
+ var matC10: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
209
+ var matC11: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
210
+ var matC12: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
211
+ var matC13: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
212
+
213
+ for (var kidx = 0u; kidx < {{ kLoopVar }}; kidx = kidx + TILE_K) {
214
+ loadSHMA(a_global_base, kidx, local_idx / 4u, local_idx % 4u);
215
+ loadSHMB(b_base, b_global_base, kidx, local_idx / {{ bKChunks }}u, local_idx % {{ bKChunks }}u);
216
+ workgroupBarrier();
217
+
218
+ for (var step = 0u; step < TILE_K; step = step + 8u) {
219
+ let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
220
+ var matA0: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>>(&tile_A, matrix_a_offset, false, TILE_K);
221
+ var matA1: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>>(&tile_A, matrix_a_offset + 8u * TILE_K, false, TILE_K);
222
+
223
+ let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
224
+ var matB0: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset, true, TILE_K);
225
+ var matB1: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 8u * TILE_K, true, TILE_K);
226
+ var matB2: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 16u * TILE_K, true, TILE_K);
227
+ var matB3: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>>(&tile_B, matrix_b_offset + 24u * TILE_K, true, TILE_K);
228
+
229
+ matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
230
+ matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
231
+ matC02 = subgroupMatrixMultiplyAccumulate(matA0, matB2, matC02);
232
+ matC03 = subgroupMatrixMultiplyAccumulate(matA0, matB3, matC03);
233
+ matC10 = subgroupMatrixMultiplyAccumulate(matA1, matB0, matC10);
234
+ matC11 = subgroupMatrixMultiplyAccumulate(matA1, matB1, matC11);
235
+ matC12 = subgroupMatrixMultiplyAccumulate(matA1, matB2, matC12);
236
+ matC13 = subgroupMatrixMultiplyAccumulate(matA1, matB3, matC13);
237
+ }
238
+ workgroupBarrier();
239
+ }
240
+
241
+ {% if fusedNarrowProjection %}
242
+ // The Conv producer owns exactly one 32-row tile. Its input tile is dead after
243
+ // the final K iteration, so reuse those 32x64 f32 cells as the only
244
+ // cross-subgroup handoff into the narrow projection. All collective stores
245
+ // remain subgroup-uniform, and every fragment has a disjoint destination.
246
+ let fused_tile_offset = base_A * TILE_COLS + base_B;
247
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 0u * TILE_COLS + 0u, matC00, false, TILE_COLS);
248
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 0u * TILE_COLS + 8u, matC01, false, TILE_COLS);
249
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 0u * TILE_COLS + 16u, matC02, false, TILE_COLS);
250
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 0u * TILE_COLS + 24u, matC03, false, TILE_COLS);
251
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 8u * TILE_COLS + 0u, matC10, false, TILE_COLS);
252
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 8u * TILE_COLS + 8u, matC11, false, TILE_COLS);
253
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 8u * TILE_COLS + 16u, matC12, false, TILE_COLS);
254
+ subgroupMatrixStore(&tile_B, fused_tile_offset + 8u * TILE_COLS + 24u, matC13, false, TILE_COLS);
255
+ workgroupBarrier();
256
+
257
+ // One invocation owns one spatial column and walks the producer channels in
258
+ // increasing order to preserve the scalar projection's accumulation order.
259
+ // The inactive half of the workgroup has no remaining collective operation
260
+ // to reach.
261
+ let fused_global_col = b_global_base + local_idx;
262
+ if (local_idx < TILE_COLS && fused_global_col < N) {
263
+ {% for oc in range(projectionOutChannels) %}
264
+ var projected_acc{{ oc }} = f32(0.0);
265
+ {% endfor %}
266
+ for (var channel = 0u; channel < M; channel = channel + 1u) {
267
+ let producer_value = tile_B[channel * TILE_COLS + local_idx]{% if hasBias %} + f32(bias[channel]){% endif %};
268
+ let activated_value = producer_value;
269
+ {% for oc in range(projectionOutChannels) %}
270
+ projected_acc{{ oc }} = projected_acc{{ oc }} + activated_value * f32(projectionW[{{ oc }}u * M + channel]);
271
+ {% endfor %}
272
+ }
273
+
274
+ let fused_y_base = batch * {{ projectionOutChannels }}u * N + fused_global_col;
275
+ {% for oc in range(projectionOutChannels) %}
276
+ let projected{{ oc }} = projected_acc{{ oc }};
277
+ let activated{{ oc }} = projected{{ oc }};
278
+ y[fused_y_base + {{ oc }}u * N] = activated{{ oc }};
279
+ {% endfor %}
280
+ }
281
+ {% elif useDirectMatrixStore %}
282
+ {% if padded %}
283
+ // Padded N only needs guarded publication for the final partial-width tile.
284
+ // Full tiles can use the same direct collective stores as the aligned path;
285
+ // the branch is uniform because b_global_base depends only on workgroup_id.x.
286
+ if (b_global_base + TILE_COLS <= N) {
287
+ {% endif %}
288
+ // Full f32 output tiles can be published by the subgroup-matrix collectives
289
+ // directly. With no epilogue this removes scratch traffic and three barriers.
290
+ // When an epilogue is present, publish the raw accumulators first, then apply
291
+ // bias, residual Z, and activation cooperatively after one storage barrier.
292
+ let matrix_c_offset = c_base + (a_global_base + base_A) * N + b_global_base + base_B;
293
+ subgroupMatrixStore(&y, matrix_c_offset + 0u * N + 0u, matC00, false, N);
294
+ subgroupMatrixStore(&y, matrix_c_offset + 0u * N + 8u, matC01, false, N);
295
+ subgroupMatrixStore(&y, matrix_c_offset + 0u * N + 16u, matC02, false, N);
296
+ subgroupMatrixStore(&y, matrix_c_offset + 0u * N + 24u, matC03, false, N);
297
+ subgroupMatrixStore(&y, matrix_c_offset + 8u * N + 0u, matC10, false, N);
298
+ subgroupMatrixStore(&y, matrix_c_offset + 8u * N + 8u, matC11, false, N);
299
+ subgroupMatrixStore(&y, matrix_c_offset + 8u * N + 16u, matC12, false, N);
300
+ subgroupMatrixStore(&y, matrix_c_offset + 8u * N + 24u, matC13, false, N);
301
+ {% if hasBias or hasActivation or hasZ %}
302
+ storageBarrier();
303
+ for (var tile_idx = local_idx; tile_idx < TILE_ROWS * TILE_COLS; tile_idx += {{ workgroupThreadsValue }}u) {
304
+ let local_row = tile_idx / TILE_COLS;
305
+ let local_col = tile_idx % TILE_COLS;
306
+ let global_row = a_global_base + local_row;
307
+ let global_col = b_global_base + local_col;
308
+ let y_index = c_base + global_row * N + global_col;
309
+ var raw = f32(y[y_index]);
310
+ {% if hasBias %}
311
+ raw += f32(bias[global_row]);
312
+ {% endif %}
313
+ {% if hasActivation or hasZ %}
314
+ y[y_index] = epi(raw{% if hasZ %}, y_index{% endif %});
315
+ {% else %}
316
+ y[y_index] = {{ T }}(raw);
317
+ {% endif %}
318
+ }
319
+ {% endif %}
320
+ {% if padded %}
321
+ return;
322
+ }
323
+ {% endif %}
324
+ {% endif %}
325
+ {% if (not useDirectMatrixStore or padded) and not splitKPartial and not fusedNarrowProjection %}
326
+ // The four scratch banks are reused across the two row-groups, and each is written
327
+ // by a collective subgroupMatrixStore then read CROSS-LANE by storeOutput. Barriers
328
+ // give the reads visibility of the store AND stop the second row-group's store from
329
+ // clobbering the first's still-in-flight readback when a partial final M-tile
330
+ // diverges storeOutput's guard. Without both barriers the last valid row can be corrupted.
331
+ subgroupMatrixStore(&scratch[subtile_id][0], 0u, matC00, false, 8u);
332
+ subgroupMatrixStore(&scratch[subtile_id][1], 0u, matC01, false, 8u);
333
+ subgroupMatrixStore(&scratch[subtile_id][2], 0u, matC02, false, 8u);
334
+ subgroupMatrixStore(&scratch[subtile_id][3], 0u, matC03, false, 8u);
335
+ workgroupBarrier();
336
+ let row = sg_id / 4u;
337
+ let col = (sg_id % 4u) * 2u;
338
+ let row_base = a_global_base + base_A;
339
+ var matrix_c_offset = c_base + row_base * N + b_global_base + base_B;
340
+ var row_limit = i32(M) - i32(row_base);
341
+ storeOutput(matrix_c_offset, {% if hasBias or polyphase %}row_base, {% endif %}row, col, subtile_id, row_limit{% if padded %}, b_global_base + base_B{% endif %});
342
+ workgroupBarrier();
343
+
344
+ subgroupMatrixStore(&scratch[subtile_id][0], 0u, matC10, false, 8u);
345
+ subgroupMatrixStore(&scratch[subtile_id][1], 0u, matC11, false, 8u);
346
+ subgroupMatrixStore(&scratch[subtile_id][2], 0u, matC12, false, 8u);
347
+ subgroupMatrixStore(&scratch[subtile_id][3], 0u, matC13, false, 8u);
348
+ workgroupBarrier();
349
+ matrix_c_offset = matrix_c_offset + 8u * N;
350
+ row_limit = i32(M) - i32(row_base + 8u);
351
+ storeOutput(matrix_c_offset, {% if hasBias or polyphase %}row_base + 8u, {% endif %}row, col, subtile_id, row_limit{% if padded %}, b_global_base + base_B{% endif %});
352
+ {% endif %}
353
+ }
build/webgpu/conv-direct-nd.wgsl.jinja ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Direct {{ source.spatialRank }}D convolution for channels-first tensors. Inputs are
2
+ // accumulated in f32 and narrowed once at the output.
3
+ // A fused epilogue may apply a residual input and activation before the store.
4
+ {% if source.usesF16 %}
5
+ enable f16;
6
+ {% endif %}
7
+ {{ env.wgsl.resourceDeclarations }}
8
+ {% set hasActivation = hasActivation is defined and hasActivation %}
9
+ {% if hasActivation %}
10
+ // Apply the fused activation in the f32 accumulator before the single output
11
+ // cast, avoiding an intermediate convolution tensor.
12
+ fn fused_act(v: f32) -> f32 {
13
+ {% if activation == "Relu" %}
14
+ return max(v, 0.0);
15
+ {% elif activation == "Clip" %}
16
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
17
+ {% elif activation == "LeakyRelu" %}
18
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
19
+ {% elif activation == "Sigmoid" %}
20
+ return 1.0 / (1.0 + exp(-v));
21
+ {% elif activation == "Tanh" %}
22
+ return tanh(v);
23
+ {% elif activation == "HardSigmoid" %}
24
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
25
+ {% else %}
26
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
27
+ {% endif %}
28
+ }
29
+ {% endif %}
30
+
31
+ const WG: u32 = {{ convWorkgroupSize }}u;
32
+
33
+ @compute @workgroup_size(WG)
34
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
35
+ @builtin(num_workgroups) nwg: vec3<u32>) {
36
+ let index = gid.x + gid.y * nwg.x * WG;
37
+ if (index >= params.count) {
38
+ return;
39
+ }
40
+
41
+ let ow = index % params.outW;
42
+ var q = index / params.outW;
43
+ let oh = q % params.outH;
44
+ q /= params.outH;
45
+ let od = q % params.outD;
46
+ q /= params.outD;
47
+ let oc = q % params.outChannels;
48
+ let batch = q / params.outChannels;
49
+ let group = oc / params.outChannelsPerGroup;
50
+
51
+ var acc = 0.0;
52
+ {% if source.hasBias %}
53
+ acc = f32(bias[oc]);
54
+ {% endif %}
55
+ for (var ic = 0u; ic < params.weightInChannels; ic += 1u) {
56
+ let inputChannel = group * params.inChannelsPerGroup + ic;
57
+ for (var kd = 0u; kd < params.kernelD; kd += 1u) {
58
+ let id = i32(od * params.strideD + kd * params.dilationD) - params.padD;
59
+ if (id < 0 || id >= i32(params.inD)) {
60
+ continue;
61
+ }
62
+ for (var kh = 0u; kh < params.kernelH; kh += 1u) {
63
+ let ih = i32(oh * params.strideH + kh * params.dilationH) - params.padH;
64
+ if (ih < 0 || ih >= i32(params.inH)) {
65
+ continue;
66
+ }
67
+ for (var kw = 0u; kw < params.kernelW; kw += 1u) {
68
+ let iw = i32(ow * params.strideW + kw * params.dilationW) - params.padW;
69
+ if (iw < 0 || iw >= i32(params.inW)) {
70
+ continue;
71
+ }
72
+ let xIndex = (((batch * params.inChannels + inputChannel) * params.inD
73
+ + u32(id)) * params.inH + u32(ih)) * params.inW + u32(iw);
74
+ let wIndex = (((oc * params.weightInChannels + ic) * params.kernelD
75
+ + kd) * params.kernelH + kh) * params.kernelW + kw;
76
+ acc += f32(x[xIndex]) * f32(w[wIndex]);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ {% if hasZ %}
82
+ // Optional residual epilogue: Y = activation(conv + B + Z); Z shares Y's layout.
83
+ acc = acc + f32(zResidual[index]);
84
+ {% endif %}
85
+ y[index] = {{ T }}({% if hasActivation %}fused_act(acc){% else %}acc{% endif %});
86
+ }
build/webgpu/conv-direct-unrolled.wgsl.jinja ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Direct NCHW 2D convolution with kernel extent, strides, dilations, and
2
+ // top/left pads compiled into the fully unrolled window below.
3
+ // One thread per output element, same flat index unpack and the same
4
+ // accumulation order (ic outer, then kh, kw ascending) as the
5
+ // scalar NCHW fallback, so f32 results are bit-identical to it.
6
+ // The kh/kw window offsets, kh*KW+kw weight offsets, and the per-channel
7
+ // strides fold to literals; the per-row bounds check is hoisted per kh.
8
+ // Supports grouped convolution exactly like the scalar fallback. f16 inputs
9
+ // are widened to an f32 accumulator and narrowed once at store.
10
+ {% if usesF16 %}
11
+ enable f16;
12
+ {% endif %}
13
+ {{ env.wgsl.resourceDeclarations }}
14
+ {% set hasActivation = hasActivation is defined and hasActivation %}
15
+ {% if hasActivation %}
16
+ // Apply the fused activation in the f32 accumulator before the single output
17
+ // cast, avoiding an intermediate convolution tensor.
18
+ fn fused_act(v: f32) -> f32 {
19
+ {% if activation == "Relu" %}
20
+ return max(v, 0.0);
21
+ {% elif activation == "Clip" %}
22
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
23
+ {% elif activation == "LeakyRelu" %}
24
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
25
+ {% elif activation == "Sigmoid" %}
26
+ return 1.0 / (1.0 + exp(-v));
27
+ {% elif activation == "Tanh" %}
28
+ return tanh(v);
29
+ {% elif activation == "HardSigmoid" %}
30
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
31
+ {% else %}
32
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
33
+ {% endif %}
34
+ }
35
+ {% endif %}
36
+
37
+ const KERNEL_AREA: u32 = {{ source.kernelH * source.kernelW }}u;
38
+ const STRIDE_H: u32 = {{ source.strideH }}u;
39
+ const STRIDE_W: u32 = {{ source.strideW }}u;
40
+ const PAD_TOP: i32 = {{ source.padTop }};
41
+ const PAD_LEFT: i32 = {{ source.padLeft }};
42
+
43
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
44
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
45
+ // 2D-folded flat index: gid.y carries the high bits past the
46
+ // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
47
+ let index = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
48
+ if (index >= params.count) {
49
+ return;
50
+ }
51
+
52
+ let ow = index % params.outW;
53
+ var t = index / params.outW;
54
+ let oh = t % params.outH;
55
+ t = t / params.outH;
56
+ let oc = t % params.outChannels;
57
+ let batch = t / params.outChannels;
58
+ let group = oc / params.outChannelsPerGroup;
59
+
60
+ let ihBase = i32(oh * STRIDE_H) - PAD_TOP;
61
+ let iwBase = i32(ow * STRIDE_W) - PAD_LEFT;
62
+ let xChannelStride = params.inH * params.inW;
63
+ var xBase = (batch * params.inChannels + group * params.inChannelsPerGroup) * xChannelStride;
64
+ var wBase = oc * params.weightInChannels * KERNEL_AREA;
65
+
66
+ var acc = 0.0;
67
+ {% if hasBias %}
68
+ acc = f32(bias[oc]);
69
+ {% endif %}
70
+ for (var ic = 0u; ic < params.weightInChannels; ic = ic + 1u) {
71
+ {% for kh in range(source.kernelH) %}
72
+ {
73
+ let ih = ihBase + {{ kh * source.dilationH }}i;
74
+ if (ih >= 0 && ih < i32(params.inH)) {
75
+ let xRow = xBase + u32(ih) * params.inW;
76
+ {% for kw in range(source.kernelW) %}
77
+ {
78
+ let iw = iwBase + {{ kw * source.dilationW }}i;
79
+ if (iw >= 0 && iw < i32(params.inW)) {
80
+ acc = acc + f32(x[xRow + u32(iw)]) * f32(w[wBase + {{ kh * source.kernelW + kw }}u]);
81
+ }
82
+ }
83
+ {% endfor %}
84
+ }
85
+ }
86
+ {% endfor %}
87
+ xBase = xBase + xChannelStride;
88
+ wBase = wBase + KERNEL_AREA;
89
+ }
90
+ {% if hasZ %}
91
+ // Optional residual epilogue: Y = activation(conv + B + Z); Z shares Y's layout.
92
+ acc = acc + f32(zResidual[index]);
93
+ {% endif %}
94
+ y[index] = {{ T }}({% if hasActivation %}fused_act(acc){% else %}acc{% endif %});
95
+ }
build/webgpu/conv-im2col-nchw.wgsl.jinja ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Materialize the convolution column matrix
2
+ // cols[b][k][pos] = X[b, ic, oh*strideH + kh*dilationH - padTop,
3
+ // ow*strideW + kw*dilationW - padLeft]
4
+ // with zero outside the input and k = (ic*KH+kh)*KW+kw. This matches the OIHW
5
+ // weight flattening, so the following GEMM consumes [M,K] x [K,outH*outW].
6
+ // gid.x grid-strides over positions for coalesced reads and writes; gid.y fixes
7
+ // k, allowing its ic/kh/kw decomposition to remain loop-invariant. The f32
8
+ // scratch round-trips f16 inputs exactly when the GEMM narrows its tile loads.
9
+ {% if usesF16 %}
10
+ enable f16;
11
+ {% endif %}
12
+ {{ env.wgsl.resourceDeclarations }}
13
+
14
+ const KERNEL_H: u32 = {{ source.kernelH }}u;
15
+ const KERNEL_W: u32 = {{ source.kernelW }}u;
16
+ const KSIZE: u32 = KERNEL_H * KERNEL_W;
17
+ const STRIDE_H: u32 = {{ source.strideH }}u;
18
+ const STRIDE_W: u32 = {{ source.strideW }}u;
19
+ const DILATION_H: u32 = {{ source.dilationH }}u;
20
+ const DILATION_W: u32 = {{ source.dilationW }}u;
21
+ const PAD_TOP: i32 = {{ source.padTop }};
22
+ const PAD_LEFT: i32 = {{ source.padLeft }};
23
+ {% if padded %}
24
+ // Padded materialization for the subgroup-matrix GEMM: the cols buffer is
25
+ // K -> ceil(K/32)*32 rows by N -> ceil(N/64)*64 columns, with rows >= kRows and
26
+ // columns >= outCount zero-filled, so every GEMM tile load is full and the zero
27
+ // region contributes exact zeros to the dot products / is never stored back.
28
+ const COLS_STRIDE: u32 = {{ nPadded }}u;
29
+ const K_PADDED: u32 = {{ kPadded }}u;
30
+
31
+ {% endif %}
32
+ @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
33
+ fn main(
34
+ @builtin(global_invocation_id) gid: vec3<u32>,
35
+ @builtin(num_workgroups) nwg: vec3<u32>
36
+ ) {
37
+ let k = gid.y;
38
+ let batch = gid.z;
39
+ // Loop-invariant: these depend on the k row alone.
40
+ let ic = k / KSIZE;
41
+ let kq = k % KSIZE;
42
+ let kh = kq / KERNEL_W;
43
+ let kw = kq % KERNEL_W;
44
+ let planeBase = (batch * params.inChannels + ic) * params.inH;
45
+ {% if padded %}
46
+ let limit = COLS_STRIDE;
47
+ {% else %}
48
+ let limit = params.outCount;
49
+ {% endif %}
50
+ let step = nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
51
+ var pos = gid.x;
52
+ loop {
53
+ if (pos >= limit) { break; }
54
+ var value = 0.0;
55
+ {% if padded %}
56
+ if (pos < params.outCount && k < params.kRows) {
57
+ {% endif %}
58
+ let oh = pos / params.outW;
59
+ let ow = pos % params.outW;
60
+ let ih = i32(oh * STRIDE_H + kh * DILATION_H) - PAD_TOP;
61
+ let iw = i32(ow * STRIDE_W + kw * DILATION_W) - PAD_LEFT;
62
+ if (ih >= 0 && ih < i32(params.inH) && iw >= 0 && iw < i32(params.inW)) {
63
+ value = f32(x[(planeBase + u32(ih)) * params.inW + u32(iw)]);
64
+ }
65
+ {% if padded %}
66
+ }
67
+ cols[(batch * K_PADDED + k) * COLS_STRIDE + pos] = value;
68
+ {% else %}
69
+ cols[(batch * params.kRows + k) * params.outCount + pos] = value;
70
+ {% endif %}
71
+ pos = pos + step;
72
+ }
73
+ }
build/webgpu/conv1d-tiled-reg.wgsl.jinja ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Implicit-GEMM register-tiled 1-D convolution over NCW: Y[b] (M x N) =
2
+ // W (M x K) @ im2col(X[b]) (K x N) with M = outChannels, K = inChannels *
3
+ // kernelW, N = outW. The im2col operand is never materialized: the X staging
4
+ // loop decodes each GEMM row into (ic, kw) and samples x with stride /
5
+ // dilation / left-pad applied, so one program covers general stride, padding,
6
+ // and dilation (group == 1 only). Workgroup geometry is tunable: a WG_X x WG_Y
7
+ // workgroup computes a (WG_Y*TM) x (WG_X*TN) output tile from BK-deep staged
8
+ // slices, each thread owning a TM x TN register micro-tile. Cooperative loads
9
+ // and stores are fully bounds-checked, so partial tiles on any edge stay
10
+ // correct for every tunable setting. f32 accumulation; bias and the optional
11
+ // activation epilogue are applied in the accumulator domain at
12
+ // store. f16 operands stay packed in workgroup memory and widen at the FMA
13
+ // site. Both tiles are indexed by their own output axis and group four K values
14
+ // per vector word, so the micro-tile accumulates through dot() and the implicit
15
+ // gather decodes one (ic, kw) tap per word, walking the remaining three. The
16
+ // micro-tile is unrolled into named registers: a dynamically indexed private
17
+ // array of vector words is large enough at the wide-M geometry that it may fail
18
+ // to scalarize and spill the accumulators.
19
+ {% if usesF16 %}
20
+ enable f16;
21
+ {% endif %}
22
+ {{ env.wgsl.resourceDeclarations }}
23
+ {% set hasActivation = hasActivation is defined and hasActivation %}
24
+ {% if hasActivation %}
25
+ // Apply the fused activation in the f32 accumulator before the single output
26
+ // cast, avoiding an intermediate convolution tensor.
27
+ fn fused_act(v: f32) -> f32 {
28
+ {% if activation == "Relu" %}
29
+ return max(v, 0.0);
30
+ {% elif activation == "Clip" %}
31
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
32
+ {% elif activation == "LeakyRelu" %}
33
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
34
+ {% elif activation == "Sigmoid" %}
35
+ return 1.0 / (1.0 + exp(-v));
36
+ {% elif activation == "Tanh" %}
37
+ return tanh(v);
38
+ {% elif activation == "HardSigmoid" %}
39
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
40
+ {% else %}
41
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
42
+ {% endif %}
43
+ }
44
+ {% endif %}
45
+
46
+ {% set tileT = "f16" if usesF16 else "f32" %}
47
+
48
+ const WG_X: u32 = {{ convWgX }}u;
49
+ const WG_Y: u32 = {{ convWgY }}u;
50
+ const THREADS: u32 = WG_X * WG_Y;
51
+ const BK: u32 = {{ convKTile }}u;
52
+ const TM: u32 = {{ convTileM }}u;
53
+ const TN: u32 = {{ convTileN }}u;
54
+ const BM: u32 = WG_Y * TM;
55
+ const BN: u32 = WG_X * TN;
56
+
57
+ fn store_out(oc: u32, ow: u32, yBase: u32, raw: f32) {
58
+ if (oc >= params.outChannels || ow >= params.outW) {
59
+ return;
60
+ }
61
+ var v = raw;
62
+ {% if hasBias %}
63
+ v = v + f32(bias[oc]);
64
+ {% endif %}
65
+ {% if hasActivation %}
66
+ v = fused_act(v);
67
+ {% endif %}
68
+ y[yBase + oc * params.outW + ow] = {{ T }}(v);
69
+ }
70
+
71
+ const K_VECS: u32 = BK / 4u;
72
+ var<workgroup> tileW: array<array<vec4<{{ tileT }}>, K_VECS>, BM>; // W[oc][k/4]
73
+ var<workgroup> tileX: array<array<vec4<{{ tileT }}>, K_VECS>, BN>; // im2col[ow][k/4]
74
+
75
+ @compute @workgroup_size(WG_X, WG_Y, 1)
76
+ fn main(
77
+ @builtin(workgroup_id) wg: vec3<u32>,
78
+ @builtin(local_invocation_id) lid: vec3<u32>
79
+ ) {
80
+ let K = params.inChannels * params.kernelW;
81
+ let ocBase = wg.y * BM;
82
+ let owBase = wg.x * BN;
83
+ let batch = wg.z;
84
+ let li = lid.y * WG_X + lid.x;
85
+
86
+ {% for i in range(convTileM) %}
87
+ {% for j in range(convTileN) %}
88
+ var acc{{ i }}_{{ j }}: f32 = 0.0;
89
+ {% endfor %}
90
+ {% endfor %}
91
+
92
+ let numTiles = (K + BK - 1u) / BK;
93
+ for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
94
+ let kBase = kt * BK;
95
+ // One vector word per lane per pass: four contiguous K values of one output
96
+ // channel, and the four K taps of one output position, whose (ic, kw) is
97
+ // decoded once and then walked.
98
+ for (var idx: u32 = li; idx < BM * K_VECS; idx = idx + THREADS) {
99
+ let wr = idx / K_VECS;
100
+ let wc4 = idx % K_VECS;
101
+ let oc = ocBase + wr;
102
+ let k = kBase + wc4 * 4u;
103
+ var word = vec4<{{ tileT }}>({{ tileT }}(0.0));
104
+ if (oc < params.outChannels) {
105
+ let rowBase = oc * K + k;
106
+ {% for component in range(4) %}
107
+ if (k + {{ component }}u < K) { word[{{ component }}u] = {{ tileT }}(w[rowBase + {{ component }}u]); }
108
+ {% endfor %}
109
+ }
110
+ tileW[wr][wc4] = word;
111
+ }
112
+ for (var idx: u32 = li; idx < BN * K_VECS; idx = idx + THREADS) {
113
+ let xc = idx / K_VECS;
114
+ let xr4 = idx % K_VECS;
115
+ let ow = owBase + xc;
116
+ let k0 = kBase + xr4 * 4u;
117
+ var word = vec4<{{ tileT }}>({{ tileT }}(0.0));
118
+ if (ow < params.outW) {
119
+ let owStride = ow * params.strideW;
120
+ let planeBase = batch * params.inChannels;
121
+ var ic = k0 / params.kernelW;
122
+ var kw = k0 % params.kernelW;
123
+ {% for component in range(4) %}
124
+ if (k0 + {{ component }}u < K) {
125
+ let iw{{ component }} = i32(owStride + kw * params.dilationW) - params.padW;
126
+ if (iw{{ component }} >= 0 && iw{{ component }} < i32(params.inW)) {
127
+ word[{{ component }}u] = {{ tileT }}(x[(planeBase + ic) * params.inW + u32(iw{{ component }})]);
128
+ }
129
+ }
130
+ kw = kw + 1u;
131
+ if (kw == params.kernelW) {
132
+ kw = 0u;
133
+ ic = ic + 1u;
134
+ }
135
+ {% endfor %}
136
+ }
137
+ tileX[xc][xr4] = word;
138
+ }
139
+ workgroupBarrier();
140
+ let tileOc = lid.y * TM;
141
+ let tileOw = lid.x * TN;
142
+ for (var kv: u32 = 0u; kv < K_VECS; kv = kv + 1u) {
143
+ {% for i in range(convTileM) %}
144
+ let wv{{ i }} = vec4<f32>(tileW[tileOc + {{ i }}u][kv]);
145
+ {% endfor %}
146
+ {% for j in range(convTileN) %}
147
+ let xv{{ j }} = vec4<f32>(tileX[tileOw + {{ j }}u][kv]);
148
+ {% endfor %}
149
+ {% for i in range(convTileM) %}
150
+ {% for j in range(convTileN) %}
151
+ acc{{ i }}_{{ j }} = acc{{ i }}_{{ j }} + dot(wv{{ i }}, xv{{ j }});
152
+ {% endfor %}
153
+ {% endfor %}
154
+ }
155
+ workgroupBarrier();
156
+ }
157
+
158
+ let yBase = batch * params.outChannels * params.outW;
159
+ let oc0 = ocBase + lid.y * TM;
160
+ let ow0 = owBase + lid.x * TN;
161
+ {% for i in range(convTileM) %}
162
+ {% for j in range(convTileN) %}
163
+ store_out(oc0 + {{ i }}u, ow0 + {{ j }}u, yBase, acc{{ i }}_{{ j }});
164
+ {% endfor %}
165
+ {% endfor %}
166
+ }
build/webgpu/conv2d-grouped-large-w4.wgsl.jinja ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% if usesF16 %}
2
+ enable f16;
3
+ {% endif %}
4
+ {{ env.wgsl.resourceDeclarations }}
5
+ {% set hasActivation = hasActivation is defined and hasActivation %}
6
+ {% if hasActivation %}
7
+ // Apply the fused activation in the f32 accumulator before the single output
8
+ // cast, avoiding an intermediate convolution tensor.
9
+ fn fused_act(v: f32) -> f32 {
10
+ {% if activation == "Relu" %}
11
+ return max(v, 0.0);
12
+ {% elif activation == "Clip" %}
13
+ return clamp(v, f32({{ actAlpha }}), f32({{ actBeta }}));
14
+ {% elif activation == "LeakyRelu" %}
15
+ return select(v * f32({{ actAlpha }}), v, v >= 0.0);
16
+ {% elif activation == "Sigmoid" %}
17
+ return 1.0 / (1.0 + exp(-v));
18
+ {% elif activation == "Tanh" %}
19
+ return tanh(v);
20
+ {% elif activation == "HardSigmoid" %}
21
+ return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
22
+ {% else %}
23
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
24
+ {% endif %}
25
+ }
26
+ {% endif %}
27
+
28
+ // Large grouped kernels are dominated by repeatedly fetching the same filter
29
+ // value for neighboring output columns and input window for neighboring output
30
+ // channels. One invocation computes four adjacent columns for OC_TILE channels,
31
+ // sharing each input load across those channels. Shape and window geometry are
32
+ // static, so all indexing divisors and kernel offsets are strength-reduced and
33
+ // unrolled by the shader compiler.
34
+ const COUNT_TILES: u32 = {{ source.countTiles }}u;
35
+ {% if source.tailOutput %}const OUT_W: u32 = max(1u, {{ source.outW }}u);
36
+ {% endif %}
37
+ const OUT_W4: u32 = max(1u, {{ source.outW4 }}u);
38
+ const OUT_H: u32 = max(1u, {{ source.outH }}u);
39
+ const OUT_C: u32 = max(1u, {{ source.outC }}u);
40
+ const OC_TILE: u32 = {{ source.ocTile }}u;
41
+ const OUT_C_TILES: u32 = OUT_C / OC_TILE;
42
+ const OUT_CPG: u32 = {{ source.outCPerGroup }}u;
43
+ const IN_C: u32 = {{ source.inC }}u;
44
+ const IN_CPG: u32 = {{ source.inCPerGroup }}u;
45
+ const IN_H: i32 = {{ source.inH }};
46
+ const IN_W: i32 = {{ source.inW }};
47
+ const IN_H_U: u32 = {{ source.inH }}u;
48
+ const IN_W_U: u32 = {{ source.inW }}u;
49
+ const IN_PLANE: u32 = IN_H_U * IN_W_U;
50
+ {% if not source.tailOutput %}const OUT_PLANE4: u32 = OUT_H * OUT_W4;
51
+ {% endif %}
52
+ const KAREA: u32 = {{ source.kernelH * source.kernelW }}u;
53
+ const STRIDE_H: u32 = {{ source.strideH }}u;
54
+ const STRIDE_W: i32 = {{ source.strideW }};
55
+ const PAD_TOP: i32 = {{ source.padTop }};
56
+ const PAD_LEFT: i32 = {{ source.padLeft }};
57
+ const WG: u32 = {{ source.workgroupSize }}u;
58
+
59
+ @compute @workgroup_size({{ source.workgroupSize }})
60
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
61
+ let q = gid.x + gid.y * nwg.x * WG;
62
+ if (q >= COUNT_TILES) { return; }
63
+
64
+ let ow4 = q % OUT_W4;
65
+ var t = q / OUT_W4;
66
+ let oh = t % OUT_H;
67
+ t /= OUT_H;
68
+ let ocBase = (t % OUT_C_TILES) * OC_TILE;
69
+ let batch = t / OUT_C_TILES;
70
+ let group = ocBase / OUT_CPG;
71
+ let ihBase = i32(oh * STRIDE_H) - PAD_TOP;
72
+ let iwBase = i32(ow4 * 4u) * STRIDE_W - PAD_LEFT;
73
+ var xChannelBase = (batch * IN_C + group * IN_CPG) * IN_PLANE;
74
+ {% for oct in range(source.ocTile) %}
75
+ var wBase{{ oct }} = (ocBase + {{ oct }}u) * IN_CPG * KAREA;
76
+ var acc{{ oct }} = vec4<f32>(0.0);
77
+ {% endfor %}
78
+
79
+ for (var ic = 0u; ic < IN_CPG; ic++) {
80
+ {% for kh in range(source.kernelH) %}
81
+ {
82
+ let ih = ihBase + {{ kh * source.dilationH }};
83
+ if (ih >= 0 && ih < IN_H) {
84
+ let xRow = xChannelBase + u32(ih) * IN_W_U;
85
+ {% if source.span <= source.spanCap %}
86
+ // The four output columns and the kernel taps overlap heavily: tap kw of
87
+ // lane l reads input column kw*dilationW + l*strideW, so kernelW*4 reads
88
+ // land on only SPAN distinct columns. Load the row window once and let
89
+ // every tap take its value from a register. Out-of-range columns hold
90
+ // zero, which contributes nothing to the accumulation exactly as the
91
+ // per-tap bounds test did.
92
+ //
93
+ // SPAN grows as (kernelW-1)*dilationW, so a widely dilated kernel would
94
+ // want more registers than a thread has and spill. Past spanCap the
95
+ // per-tap form below stays, where the register cost is fixed.
96
+ {% for s in range(source.span) %}
97
+ {% set used = namespace(value=false) %}
98
+ {% for kw in range(source.kernelW) %}
99
+ {% for lane in range(4) %}
100
+ {% if s == kw * source.dilationW + lane * source.strideW %}{% set used.value = true %}{% endif %}
101
+ {% endfor %}
102
+ {% endfor %}
103
+ {% if used.value %}
104
+ var xw{{ s }} = 0.0;
105
+ {% endif %}
106
+ {% endfor %}
107
+ {% for s in range(source.span) %}
108
+ {% set used = namespace(value=false) %}
109
+ {% for kw in range(source.kernelW) %}
110
+ {% for lane in range(4) %}
111
+ {% if s == kw * source.dilationW + lane * source.strideW %}{% set used.value = true %}{% endif %}
112
+ {% endfor %}
113
+ {% endfor %}
114
+ {% if used.value %}
115
+ {
116
+ let iw = iwBase + {{ s }};
117
+ if (iw >= 0 && iw < IN_W) { xw{{ s }} = f32(x[xRow + u32(iw)]); }
118
+ }
119
+ {% endif %}
120
+ {% endfor %}
121
+ {% for kw in range(source.kernelW) %}
122
+ {
123
+ {% for oct in range(source.ocTile) %}
124
+ let weight{{ oct }} = f32(w[wBase{{ oct }} + {{ kh * source.kernelW + kw }}u]);
125
+ {% endfor %}
126
+ {% for lane in range(4) %}
127
+ {% for oct in range(source.ocTile) %}
128
+ acc{{ oct }}.{{ ["x", "y", "z", "w"][lane] }} += xw{{ kw * source.dilationW + lane * source.strideW }} * weight{{ oct }};
129
+ {% endfor %}
130
+ {% endfor %}
131
+ }
132
+ {% endfor %}
133
+ {% else %}
134
+ {% for kw in range(source.kernelW) %}
135
+ {
136
+ {% for oct in range(source.ocTile) %}
137
+ let weight{{ oct }} = f32(w[wBase{{ oct }} + {{ kh * source.kernelW + kw }}u]);
138
+ {% endfor %}
139
+ let iwK = iwBase + {{ kw * source.dilationW }};
140
+ {% for lane in range(4) %}
141
+ let iwLane{{ lane }} = iwK + {{ lane * source.strideW }};
142
+ if (iwLane{{ lane }} >= 0 && iwLane{{ lane }} < IN_W) {
143
+ let xValue = f32(x[xRow + u32(iwLane{{ lane }})]);
144
+ {% for oct in range(source.ocTile) %}
145
+ acc{{ oct }}.{{ ["x", "y", "z", "w"][lane] }} += xValue * weight{{ oct }};
146
+ {% endfor %}
147
+ }
148
+ {% endfor %}
149
+ }
150
+ {% endfor %}
151
+ {% endif %}
152
+ }
153
+ }
154
+ {% endfor %}
155
+ xChannelBase += IN_PLANE;
156
+ {% for oct in range(source.ocTile) %}
157
+ wBase{{ oct }} += KAREA;
158
+ {% endfor %}
159
+ }
160
+ {% if source.tailOutput %}
161
+ let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W + ow4 * 4u;
162
+ {% for oct in range(source.ocTile) %}
163
+ let outChannelBase{{ oct }} = outBase + {{ oct }}u * OUT_H * OUT_W;
164
+ {% for lane in range(4) %}
165
+ if (ow4 * 4u + {{ lane }}u < OUT_W) {
166
+ y[outChannelBase{{ oct }} + {{ lane }}u] = {{ scalar }}({% if hasActivation %}fused_act(acc{{ oct }}.{{ ["x", "y", "z", "w"][lane] }}){% else %}acc{{ oct }}.{{ ["x", "y", "z", "w"][lane] }}{% endif %});
167
+ }
168
+ {% endfor %}
169
+ {% endfor %}
170
+ {% else %}
171
+ let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W4 + ow4;
172
+ {% for oct in range(source.ocTile) %}
173
+ y[outBase + {{ oct }}u * OUT_PLANE4] = {{ vectorScalar }}({% if hasActivation %}vec4<f32>(fused_act(acc{{ oct }}.x), fused_act(acc{{ oct }}.y), fused_act(acc{{ oct }}.z), fused_act(acc{{ oct }}.w)){% else %}acc{{ oct }}{% endif %});
174
+ {% endfor %}
175
+ {% endif %}
176
+ }
build/webgpu/manifest.json ADDED
The diff for this file is too large to render. See raw diff
 
build/webgpu/metadata.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "com.microsoft.FusedConv",
3
+ "id": "_com_microsoft_fusedconv_webgpu_947df1c",
4
+ "version": 1,
5
+ "license": "Apache-2.0",
6
+ "backend": { "type": "webgpu" },
7
+ "digest": {
8
+ "algorithm": "sha256",
9
+ "files": {
10
+ "bench.json": "0T4EaJworpji9PrXCF3znUUqiMBBbSgShzzAaM/dOWY=",
11
+ "conv-1x1-gemm-tiled-reg.wgsl.jinja": "bDk/sW4LxlVlqVJ6L/hpKGjfky1UKWwYL3C0fdVWjFA=",
12
+ "conv-1x1-gemm-tiled.wgsl.jinja": "eHdHLa3X2HriPxhMMSmzkVPelPg+sm4xDz9ZJ4qI0hA=",
13
+ "conv-1x1-subgroup-matrix.wgsl.jinja": "FTHq1ftW5Zk7as0fc8T+b0dvbQFQFmcX0AWARnICV28=",
14
+ "conv-direct-nd.wgsl.jinja": "yEas2jFgJNfM8NoHjv+4ZafMC1/nwl5MvgQeTFfOl4w=",
15
+ "conv-direct-unrolled.wgsl.jinja": "84nok2Ie1Lc91UrXiUEl1GFk+cwa4Fu0vEkhRyOmsxs=",
16
+ "conv-im2col-nchw.wgsl.jinja": "7IUuXo33elZx0pnR9M2vUXzXDjFuw2bJODkZViV/WQg=",
17
+ "conv1d-tiled-reg.wgsl.jinja": "d9H8LBgSgsqxBbg85o5dCMuOEIW/hOICoEi5gysrvTg=",
18
+ "conv2d-grouped-large-w4.wgsl.jinja": "jhLKpmvfkcFQPBXXznt3im++rtVwNNxUoRqqgHNpmDM=",
19
+ "manifest.json": "jMjmEiSVgMOjNheSacCaHlppVU7uWplUp2lv3fvGH1A=",
20
+ "test.json": "/KodhC9ZJ6IxO1wokLmeGH0zsOfDpds9+mjZLZRnPXw="
21
+ }
22
+ },
23
+ "provenance": { "kernel": { "sha": "2e7068faf55e7f43df740015f6d1ee49391a41c5", "dirty": false } },
24
+ "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.FusedConv" }
25
+ }
build/webgpu/test.json ADDED
The diff for this file is too large to render. See raw diff