Xenova HF Staff commited on
Commit
9008499
·
verified ·
1 Parent(s): 85afcec

sync 91d990483a17

Browse files
README.md CHANGED
@@ -18,18 +18,18 @@ See the [ONNX Runtime `FusedConv` contrib-operator spec](https://github.com/micr
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
 
@@ -37,11 +37,11 @@ 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. |
@@ -52,13 +52,48 @@ Attributes and default values (overridable per request):
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
@@ -68,15 +103,20 @@ Some implementation variants require `subgroup-matrix` and `subgroups`. These ar
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
 
 
18
 
19
  ## Inputs
20
 
21
+ | Name | Upstream name | 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
+ | `bias` | `B` | `T` | `1` | — | Optional 1-D bias tensor of length `out_channels`, broadcast-added to each output channel. | optional |
26
+ | `zResidual` | `Z` | `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 | Upstream name | Logical dtype | Rank | Shape | Description | Presence |
31
  | --- | --- | --- | --- | --- | --- | --- |
32
+ | `y` | `Y` | `T` | same as `x` | derived | Output feature map tensor after convolution, optional bias/residual addition, and the fused activation. | required |
33
 
34
  ## Attributes
35
 
 
37
 
38
  | Attribute | Default | Description |
39
  | --- | --- | --- |
 
 
40
  | `activation` | — | Optional fused activation name: `Relu`, `LeakyRelu`, `Sigmoid`, `Tanh`, `HardSigmoid`, `HardSwish`, or `Clip`. Omission applies no activation. |
41
  | `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. |
42
+ | `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. |
43
  | `dilations` | — | Optional dilation factors, one positive integer per spatial axis. Omission means all ones. |
44
+ | `group` | `1` | Number of groups that input and output channels are split into; defaults to 1. |
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. |
 
52
  | --- | --- |
53
  | `T` | `float32`, `float16` |
54
 
55
+ ## Implementation variants
56
+
57
+ One implementation is selected per call from the device capabilities, the request shapes and the dtypes; these notes say what each one covers.
58
+
59
+ - `implicit_im2col_tiled_reg_splitk` — Partitions the register-blocked implicit-im2col reduction across workgroups when the unsplit output grid is too small, then combines raw partial sums and applies the bias and fused activation once. The combine reassociates floating-point addition relative to the unsplit route.
60
+ - `implicit_im2col_tiled_bias_reg_splitk` — Partitions the register-blocked implicit-im2col reduction across workgroups when the unsplit output grid is too small, then combines raw partial sums and applies the bias and fused activation once. The combine reassociates floating-point addition relative to the unsplit route.
61
+ - `implicit_im2col_tiled_reg_splitk_preferred` — Partitions the register-blocked implicit-im2col reduction across workgroups when the unsplit output grid is too small, then combines raw partial sums and applies the bias and fused activation once. The combine reassociates floating-point addition relative to the unsplit route.
62
+ - `implicit_im2col_tiled_bias_reg_splitk_preferred` — Partitions the register-blocked implicit-im2col reduction across workgroups when the unsplit output grid is too small, then combines raw partial sums and applies the bias and fused activation once. The combine reassociates floating-point addition relative to the unsplit route.
63
+ - `implicit_im2col_tiled_reg` — Gathers logical im2col elements directly from `X` into register-blocked GEMM tiles and applies the fused activation in the epilogue, avoiding a materialized column scratch buffer.
64
+ - `implicit_im2col_tiled_bias_reg` — Gathers logical im2col elements directly from `X` into register-blocked GEMM tiles and applies bias and the fused activation in the epilogue, avoiding a materialized column scratch buffer.
65
+ - `gemm_1x1_subgroup_matrix` — Multiplies 1x1 weights directly by NCHW input, then applies the selected bias, residual and activation epilogue. Complete output-channel tiles bypass input staging and reduction-loop barriers; partial tiles retain guarded staging. Tile geometry follows the device's workgroup limits.
66
+ - `gemm_1x1_subgroup_matrix_z` — Multiplies 1x1 weights directly by NCHW input, then applies the selected bias, residual and activation epilogue. Complete output-channel tiles bypass input staging and reduction-loop barriers; partial tiles retain guarded staging. Tile geometry follows the device's workgroup limits.
67
+ - `gemm_1x1_subgroup_matrix_bias` — Multiplies 1x1 weights directly by NCHW input, then applies the selected bias, residual and activation epilogue. Complete output-channel tiles bypass input staging and reduction-loop barriers; partial tiles retain guarded staging. Tile geometry follows the device's workgroup limits.
68
+ - `gemm_1x1_subgroup_matrix_bias_z` — Multiplies 1x1 weights directly by NCHW input, then applies the selected bias, residual and activation epilogue. Complete output-channel tiles bypass input staging and reduction-loop barriers; partial tiles retain guarded staging. Tile geometry follows the device's workgroup limits.
69
+ - `im2col_gemm_subgroup_matrix` — Multiply aligned materialized columns through workgroup-staged subgroup matrices with f32 accumulation and the fused epilogue.
70
+ - `im2col_direct_inputs_subgroup_matrix` — Multiply aligned materialized f32 columns through direct subgroup-matrix loads, retaining the f32 accumulation order and fused epilogue while omitting operand staging and K-loop barriers.
71
+ - `im2col_gemm_subgroup_matrix_z` — Multiply aligned materialized columns through workgroup-staged subgroup matrices with f32 accumulation and the fused epilogue.
72
+ - `im2col_direct_inputs_subgroup_matrix_z` — Multiply aligned materialized f32 columns through direct subgroup-matrix loads, retaining the f32 accumulation order and fused epilogue while omitting operand staging and K-loop barriers.
73
+ - `im2col_gemm_subgroup_matrix_bias` — Multiply aligned materialized columns through workgroup-staged subgroup matrices with f32 accumulation and the fused epilogue.
74
+ - `im2col_direct_inputs_subgroup_matrix_bias` — Multiply aligned materialized f32 columns through direct subgroup-matrix loads, retaining the f32 accumulation order and fused epilogue while omitting operand staging and K-loop barriers.
75
+ - `im2col_gemm_subgroup_matrix_bias_z` — Multiply aligned materialized columns through workgroup-staged subgroup matrices with f32 accumulation and the fused epilogue.
76
+ - `im2col_direct_inputs_subgroup_matrix_bias_z` — Multiply aligned materialized f32 columns through direct subgroup-matrix loads, retaining the f32 accumulation order and fused epilogue while omitting operand staging and K-loop barriers.
77
+ - `im2col_half_direct_subgroup_matrix` — Materialize aligned f16 convolution columns without widening their storage, then load weights and columns directly into subgroup matrices. Preserve f32 accumulation and the existing bias, residual, activation and f16 output rounding while removing operand staging and K-loop barriers.
78
+ - `im2col_half_direct_subgroup_matrix_z` — Materialize aligned f16 convolution columns without widening their storage, then load weights and columns directly into subgroup matrices. Preserve f32 accumulation and the existing bias, residual, activation and f16 output rounding while removing operand staging and K-loop barriers.
79
+ - `im2col_half_direct_subgroup_matrix_bias` — Materialize aligned f16 convolution columns without widening their storage, then load weights and columns directly into subgroup matrices. Preserve f32 accumulation and the existing bias, residual, activation and f16 output rounding while removing operand staging and K-loop barriers.
80
+ - `im2col_half_direct_subgroup_matrix_bias_z` — Materialize aligned f16 convolution columns without widening their storage, then load weights and columns directly into subgroup matrices. Preserve f32 accumulation and the existing bias, residual, activation and f16 output rounding while removing operand staging and K-loop barriers.
81
+ - `implicit_im2col_subgroup_matrix` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
82
+ - `implicit_im2col_subgroup_matrix_z` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
83
+ - `implicit_im2col_subgroup_matrix_bias` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
84
+ - `implicit_im2col_subgroup_matrix_bias_z` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
85
+ - `implicit_im2col_subgroup_matrix_f16` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
86
+ - `implicit_im2col_subgroup_matrix_z_f16` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
87
+ - `implicit_im2col_subgroup_matrix_bias_f16` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
88
+ - `implicit_im2col_subgroup_matrix_bias_z_f16` — Gathers convolution input tiles directly into subgroup-matrix operands and applies the existing bias, residual, and activation epilogue, avoiding a materialized column matrix when output-channel reuse permits or both column layouts exceed device allocation limits.
89
+
90
  ## Device requirements
91
 
92
+ Some implementation variants require `subgroup-matrix`, `shader-f16`, and `subgroups`. These are route-specific capabilities, not package-wide requirements; availability also depends on the request shape and dtype.
93
 
94
  ## Files
95
 
96
+ - [`metadata.json`](build/webgpu/metadata.json) — kernel metadata (id, digests, per-variant templates, provenance)
97
  - [`manifest.json`](build/webgpu/manifest.json) — the op contract (source of truth)
98
  - [`test.json`](build/webgpu/test.json) — correctness cases
99
  - [`bench.json`](build/webgpu/bench.json) — benchmark + tuning cases
 
103
  - [`conv-direct-nd.wgsl.jinja`](build/webgpu/conv-direct-nd.wgsl.jinja)
104
  - [`conv-direct-unrolled.wgsl.jinja`](build/webgpu/conv-direct-unrolled.wgsl.jinja)
105
  - [`conv-im2col-nchw.wgsl.jinja`](build/webgpu/conv-im2col-nchw.wgsl.jinja)
106
+ - [`conv-splitk-reduce.wgsl.jinja`](build/webgpu/conv-splitk-reduce.wgsl.jinja)
107
  - [`conv1d-tiled-reg.wgsl.jinja`](build/webgpu/conv1d-tiled-reg.wgsl.jinja)
108
  - [`conv2d-grouped-large-w4.wgsl.jinja`](build/webgpu/conv2d-grouped-large-w4.wgsl.jinja)
109
 
110
  ## Use with `@huggingface/kernels`
111
 
112
+ ```sh
113
+ npm install --save-exact @huggingface/kernels@0.0.1-preview.2
114
+ ```
115
+
116
+ Required output shapes and logical data types are inferred from the supplied inputs and attributes; result tensors are allocated automatically.
117
 
118
  The `version: 1` option selects the published kernel contract; it is independent of any operator opset, contrib `since_version`, or model version.
119
+ It follows the `v1` branch as fixes land. To pin exact artifact bytes, pass a 40-character commit `revision` instead of `version`.
120
 
121
  Replace each `*Data` placeholder with a typed array containing the corresponding input data.
122
 
build/webgpu/bench.json CHANGED
@@ -1,5 +1,4 @@
1
  {
2
- "op": "com.microsoft.FusedConv",
3
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
4
  "cases": [
5
  {
@@ -227,8 +226,8 @@
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,
@@ -667,8 +666,8 @@
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,
@@ -826,7 +825,7 @@
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": {
@@ -861,7 +860,7 @@
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": {
@@ -898,7 +897,7 @@
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] },
@@ -917,6 +916,459 @@
917
  }
918
  ]
919
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
920
  }
921
  ]
922
  }
 
1
  {
 
2
  "tunableSpace": { "WORKGROUP_SIZE": [64, 128, 256] },
3
  "cases": [
4
  {
 
226
  "name": "fusedconv-f32-1x1-bias-b1c256m256-32x32",
227
  "preset": "smoke",
228
  "provenance": {
229
+ "source": "synthetic",
230
+ "notes": "A biased 1x1 convolution produces 64 register tiles, measuring the register-tiled route at its admission floor."
231
  },
232
  "vars": {
233
  "batch": 1,
 
666
  "name": "fusedconv-3x3-dense-bias-c256m256-32x32",
667
  "preset": "smoke",
668
  "provenance": {
669
+ "source": "synthetic",
670
+ "notes": "A biased 3x3 convolution produces 64 register tiles, measuring the register-tiled im2col route at its admission floor."
671
  },
672
  "vars": {
673
  "batch": 1,
 
825
  {
826
  "name": "fusedconv-group2-k7-c64m64-64x64-direct-fallback",
827
  "provenance": {
828
+ "notes": "A group-2, 7x7 convolution with wide output exercises the four-wide grouped large-kernel route."
829
  },
830
  "preset": "stress",
831
  "vars": {
 
860
  {
861
  "name": "fusedconv-group4-k5-c128m128-48x48-direct-fallback",
862
  "provenance": {
863
+ "notes": "A group-4, 5x5 convolution exercises the four-wide grouped large-kernel route near its kernel-size boundary."
864
  },
865
  "preset": "stress",
866
  "vars": {
 
897
  "preset": "stress",
898
  "provenance": {
899
  "source": "ONNX Runtime FusedConv provider semantics",
900
+ "notes": "A large aligned temporal convolution with bias and fused HardSwish exercises the register-tiled implicit-GEMM path."
901
  },
902
  "vars": { "batch": 1, "inChannels": 80, "outChannels": 512, "inW": 3000, "kernelW": 3, "strideW": 1, "padW": 1 },
903
  "attrs": { "activation": "HardSwish", "strides": [1], "pads": [1, 1] },
 
916
  }
917
  ]
918
  }
919
+ },
920
+ {
921
+ "name": "fusedconv-splitk-grid32-b1m128-32x32",
922
+ "preset": "smoke",
923
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
924
+ "inputs": {
925
+ "x": { "shape": [1, 64, 32, 32], "dtype": "float32", "dist": "normal", "seed": 7770, "scale": 0.2 },
926
+ "w": { "shape": [128, 64, 3, 3], "dtype": "float32", "dist": "normal", "seed": 2676, "scale": 0.05 }
927
+ },
928
+ "outputs": { "y": { "shape": [1, 128, 32, 32], "dtype": "float32" } },
929
+ "bench": {
930
+ "metrics": [
931
+ { "type": "gflops", "value": "2 * numel(shapes.y) * dim(shapes.w, 1) * dim(shapes.w, 2) * dim(shapes.w, 3)" }
932
+ ]
933
+ },
934
+ "provenance": {
935
+ "source": "synthetic",
936
+ "notes": "Measures the 32-workgroup implicit split-K preference boundary with output channels, spatial extent, or batch contributing to the grid."
937
+ }
938
+ },
939
+ {
940
+ "name": "fusedconv-splitk-grid32-b1m64-32x64",
941
+ "preset": "smoke",
942
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
943
+ "inputs": {
944
+ "x": { "shape": [1, 64, 32, 64], "dtype": "float32", "dist": "normal", "seed": 7770, "scale": 0.2 },
945
+ "w": { "shape": [64, 64, 3, 3], "dtype": "float32", "dist": "normal", "seed": 2676, "scale": 0.05 }
946
+ },
947
+ "outputs": { "y": { "shape": [1, 64, 32, 64], "dtype": "float32" } },
948
+ "bench": {
949
+ "metrics": [
950
+ { "type": "gflops", "value": "2 * numel(shapes.y) * dim(shapes.w, 1) * dim(shapes.w, 2) * dim(shapes.w, 3)" }
951
+ ]
952
+ },
953
+ "provenance": {
954
+ "source": "synthetic",
955
+ "notes": "Measures the 32-workgroup implicit split-K preference boundary with output channels, spatial extent, or batch contributing to the grid."
956
+ }
957
+ },
958
+ {
959
+ "name": "fusedconv-splitk-grid32-b2m64-32x32",
960
+ "preset": "smoke",
961
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
962
+ "inputs": {
963
+ "x": { "shape": [2, 64, 32, 32], "dtype": "float32", "dist": "normal", "seed": 7770, "scale": 0.2 },
964
+ "w": { "shape": [64, 64, 3, 3], "dtype": "float32", "dist": "normal", "seed": 2676, "scale": 0.05 }
965
+ },
966
+ "outputs": { "y": { "shape": [2, 64, 32, 32], "dtype": "float32" } },
967
+ "bench": {
968
+ "metrics": [
969
+ { "type": "gflops", "value": "2 * numel(shapes.y) * dim(shapes.w, 1) * dim(shapes.w, 2) * dim(shapes.w, 3)" }
970
+ ]
971
+ },
972
+ "provenance": {
973
+ "source": "synthetic",
974
+ "notes": "Measures the 32-workgroup implicit split-K preference boundary with output channels, spatial extent, or batch contributing to the grid."
975
+ }
976
+ },
977
+ {
978
+ "name": "fusedconv-direct-inputs-row-band-m32-k288-n256",
979
+ "preset": "smoke",
980
+ "vars": {
981
+ "batch": 1,
982
+ "inChannels": 32,
983
+ "outChannels": 32,
984
+ "inH": 16,
985
+ "inW": 16,
986
+ "kernelH": 3,
987
+ "kernelW": 3,
988
+ "strideH": 1,
989
+ "strideW": 1,
990
+ "padH": 1,
991
+ "padW": 1
992
+ },
993
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
994
+ "inputs": {
995
+ "x": { "shape": [1, 32, 16, 16], "dtype": "float32", "dist": "normal", "seed": 839, "scale": 0.2 },
996
+ "w": { "shape": [32, 32, 3, 3], "dtype": "float32", "dist": "normal", "seed": 1375, "scale": 0.05 }
997
+ },
998
+ "outputs": { "y": { "shape": [1, 32, 16, 16], "dtype": "float32" } },
999
+ "bench": {
1000
+ "metrics": [
1001
+ {
1002
+ "type": "gflops",
1003
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1004
+ }
1005
+ ]
1006
+ }
1007
+ },
1008
+ {
1009
+ "name": "fusedconv-direct-inputs-row-band-m64-k288-n256",
1010
+ "preset": "smoke",
1011
+ "vars": {
1012
+ "batch": 1,
1013
+ "inChannels": 32,
1014
+ "outChannels": 64,
1015
+ "inH": 16,
1016
+ "inW": 16,
1017
+ "kernelH": 3,
1018
+ "kernelW": 3,
1019
+ "strideH": 1,
1020
+ "strideW": 1,
1021
+ "padH": 1,
1022
+ "padW": 1
1023
+ },
1024
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
1025
+ "inputs": {
1026
+ "x": { "shape": [1, 32, 16, 16], "dtype": "float32", "dist": "normal", "seed": 839, "scale": 0.2 },
1027
+ "w": { "shape": [64, 32, 3, 3], "dtype": "float32", "dist": "normal", "seed": 1375, "scale": 0.05 }
1028
+ },
1029
+ "outputs": { "y": { "shape": [1, 64, 16, 16], "dtype": "float32" } },
1030
+ "bench": {
1031
+ "metrics": [
1032
+ {
1033
+ "type": "gflops",
1034
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1035
+ }
1036
+ ]
1037
+ }
1038
+ },
1039
+ {
1040
+ "name": "fusedconv-half-direct-inputs-row-band-m32-k288-n256",
1041
+ "preset": "smoke",
1042
+ "vars": {
1043
+ "batch": 1,
1044
+ "inChannels": 32,
1045
+ "outChannels": 32,
1046
+ "inH": 16,
1047
+ "inW": 16,
1048
+ "kernelH": 3,
1049
+ "kernelW": 3,
1050
+ "strideH": 1,
1051
+ "strideW": 1,
1052
+ "padH": 1,
1053
+ "padW": 1
1054
+ },
1055
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
1056
+ "inputs": {
1057
+ "x": { "shape": [1, 32, 16, 16], "dtype": "float16", "dist": "normal", "seed": 839, "scale": 0.2 },
1058
+ "w": { "shape": [32, 32, 3, 3], "dtype": "float16", "dist": "normal", "seed": 1375, "scale": 0.05 }
1059
+ },
1060
+ "outputs": { "y": { "shape": [1, 32, 16, 16], "dtype": "float16" } },
1061
+ "bench": {
1062
+ "metrics": [
1063
+ {
1064
+ "type": "gflops",
1065
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1066
+ }
1067
+ ]
1068
+ }
1069
+ },
1070
+ {
1071
+ "name": "fusedconv-half-direct-inputs-row-band-m64-k288-n256",
1072
+ "preset": "smoke",
1073
+ "vars": {
1074
+ "batch": 1,
1075
+ "inChannels": 32,
1076
+ "outChannels": 64,
1077
+ "inH": 16,
1078
+ "inW": 16,
1079
+ "kernelH": 3,
1080
+ "kernelW": 3,
1081
+ "strideH": 1,
1082
+ "strideW": 1,
1083
+ "padH": 1,
1084
+ "padW": 1
1085
+ },
1086
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
1087
+ "inputs": {
1088
+ "x": { "shape": [1, 32, 16, 16], "dtype": "float16", "dist": "normal", "seed": 839, "scale": 0.2 },
1089
+ "w": { "shape": [64, 32, 3, 3], "dtype": "float16", "dist": "normal", "seed": 1375, "scale": 0.05 }
1090
+ },
1091
+ "outputs": { "y": { "shape": [1, 64, 16, 16], "dtype": "float16" } },
1092
+ "bench": {
1093
+ "metrics": [
1094
+ {
1095
+ "type": "gflops",
1096
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1097
+ }
1098
+ ]
1099
+ }
1100
+ },
1101
+ {
1102
+ "name": "fusedconv-half-direct-inputs-row-band-m96-k288-n256",
1103
+ "preset": "smoke",
1104
+ "vars": {
1105
+ "batch": 1,
1106
+ "inChannels": 32,
1107
+ "outChannels": 96,
1108
+ "inH": 16,
1109
+ "inW": 16,
1110
+ "kernelH": 3,
1111
+ "kernelW": 3,
1112
+ "strideH": 1,
1113
+ "strideW": 1,
1114
+ "padH": 1,
1115
+ "padW": 1
1116
+ },
1117
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
1118
+ "inputs": {
1119
+ "x": { "shape": [1, 32, 16, 16], "dtype": "float16", "dist": "normal", "seed": 839, "scale": 0.2 },
1120
+ "w": { "shape": [96, 32, 3, 3], "dtype": "float16", "dist": "normal", "seed": 1375, "scale": 0.05 }
1121
+ },
1122
+ "outputs": { "y": { "shape": [1, 96, 16, 16], "dtype": "float16" } },
1123
+ "bench": {
1124
+ "metrics": [
1125
+ {
1126
+ "type": "gflops",
1127
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1128
+ }
1129
+ ]
1130
+ }
1131
+ },
1132
+ {
1133
+ "name": "fusedconv-half-direct-inputs-row-band-m128-k288-n256",
1134
+ "preset": "smoke",
1135
+ "vars": {
1136
+ "batch": 1,
1137
+ "inChannels": 32,
1138
+ "outChannels": 128,
1139
+ "inH": 16,
1140
+ "inW": 16,
1141
+ "kernelH": 3,
1142
+ "kernelW": 3,
1143
+ "strideH": 1,
1144
+ "strideW": 1,
1145
+ "padH": 1,
1146
+ "padW": 1
1147
+ },
1148
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [1, 1, 1, 1] },
1149
+ "inputs": {
1150
+ "x": { "shape": [1, 32, 16, 16], "dtype": "float16", "dist": "normal", "seed": 839, "scale": 0.2 },
1151
+ "w": { "shape": [128, 32, 3, 3], "dtype": "float16", "dist": "normal", "seed": 1375, "scale": 0.05 }
1152
+ },
1153
+ "outputs": { "y": { "shape": [1, 128, 16, 16], "dtype": "float16" } },
1154
+ "bench": {
1155
+ "metrics": [
1156
+ {
1157
+ "type": "gflops",
1158
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1159
+ }
1160
+ ]
1161
+ }
1162
+ },
1163
+ {
1164
+ "name": "direct-1x1-float32-m63-k512-n4096",
1165
+ "preset": "model",
1166
+ "vars": {
1167
+ "batch": 1,
1168
+ "inChannels": 512,
1169
+ "outChannels": 63,
1170
+ "inH": 64,
1171
+ "inW": 64,
1172
+ "kernelH": 1,
1173
+ "kernelW": 1,
1174
+ "strideH": 1,
1175
+ "strideW": 1,
1176
+ "padH": 0,
1177
+ "padW": 0
1178
+ },
1179
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
1180
+ "inputs": {
1181
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float32", "dist": "normal", "seed": 354, "scale": 0.2 },
1182
+ "w": { "shape": [63, 512, 1, 1], "dtype": "float32", "dist": "normal", "seed": 355, "scale": 0.02 }
1183
+ },
1184
+ "outputs": { "y": { "shape": [1, 63, 64, 64], "dtype": "float32" } },
1185
+ "bench": {
1186
+ "metrics": [
1187
+ {
1188
+ "type": "gflops",
1189
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1190
+ }
1191
+ ]
1192
+ },
1193
+ "provenance": {
1194
+ "source": "synthetic",
1195
+ "notes": "Row-tile boundary sweep: full, partial, and device-dependent complete row bands share the same reduction and spatial extent."
1196
+ }
1197
+ },
1198
+ {
1199
+ "name": "direct-1x1-float32-m64-k512-n4096",
1200
+ "preset": "model",
1201
+ "vars": {
1202
+ "batch": 1,
1203
+ "inChannels": 512,
1204
+ "outChannels": 64,
1205
+ "inH": 64,
1206
+ "inW": 64,
1207
+ "kernelH": 1,
1208
+ "kernelW": 1,
1209
+ "strideH": 1,
1210
+ "strideW": 1,
1211
+ "padH": 0,
1212
+ "padW": 0
1213
+ },
1214
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
1215
+ "inputs": {
1216
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float32", "dist": "normal", "seed": 354, "scale": 0.2 },
1217
+ "w": { "shape": [64, 512, 1, 1], "dtype": "float32", "dist": "normal", "seed": 355, "scale": 0.02 }
1218
+ },
1219
+ "outputs": { "y": { "shape": [1, 64, 64, 64], "dtype": "float32" } },
1220
+ "bench": {
1221
+ "metrics": [
1222
+ {
1223
+ "type": "gflops",
1224
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1225
+ }
1226
+ ]
1227
+ },
1228
+ "provenance": {
1229
+ "source": "synthetic",
1230
+ "notes": "Row-tile boundary sweep: full, partial, and device-dependent complete row bands share the same reduction and spatial extent."
1231
+ }
1232
+ },
1233
+ {
1234
+ "name": "direct-1x1-float32-m96-k512-n4096",
1235
+ "preset": "model",
1236
+ "vars": {
1237
+ "batch": 1,
1238
+ "inChannels": 512,
1239
+ "outChannels": 96,
1240
+ "inH": 64,
1241
+ "inW": 64,
1242
+ "kernelH": 1,
1243
+ "kernelW": 1,
1244
+ "strideH": 1,
1245
+ "strideW": 1,
1246
+ "padH": 0,
1247
+ "padW": 0
1248
+ },
1249
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
1250
+ "inputs": {
1251
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float32", "dist": "normal", "seed": 354, "scale": 0.2 },
1252
+ "w": { "shape": [96, 512, 1, 1], "dtype": "float32", "dist": "normal", "seed": 355, "scale": 0.02 }
1253
+ },
1254
+ "outputs": { "y": { "shape": [1, 96, 64, 64], "dtype": "float32" } },
1255
+ "bench": {
1256
+ "metrics": [
1257
+ {
1258
+ "type": "gflops",
1259
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1260
+ }
1261
+ ]
1262
+ },
1263
+ "provenance": {
1264
+ "source": "synthetic",
1265
+ "notes": "Row-tile boundary sweep: full, partial, and device-dependent complete row bands share the same reduction and spatial extent."
1266
+ }
1267
+ },
1268
+ {
1269
+ "name": "direct-1x1-float16-m63-k512-n4096",
1270
+ "preset": "model",
1271
+ "vars": {
1272
+ "batch": 1,
1273
+ "inChannels": 512,
1274
+ "outChannels": 63,
1275
+ "inH": 64,
1276
+ "inW": 64,
1277
+ "kernelH": 1,
1278
+ "kernelW": 1,
1279
+ "strideH": 1,
1280
+ "strideW": 1,
1281
+ "padH": 0,
1282
+ "padW": 0
1283
+ },
1284
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
1285
+ "inputs": {
1286
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float16", "dist": "normal", "seed": 354, "scale": 0.2 },
1287
+ "w": { "shape": [63, 512, 1, 1], "dtype": "float16", "dist": "normal", "seed": 355, "scale": 0.02 }
1288
+ },
1289
+ "outputs": { "y": { "shape": [1, 63, 64, 64], "dtype": "float16" } },
1290
+ "bench": {
1291
+ "metrics": [
1292
+ {
1293
+ "type": "gflops",
1294
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1295
+ }
1296
+ ]
1297
+ },
1298
+ "provenance": {
1299
+ "source": "synthetic",
1300
+ "notes": "Row-tile boundary sweep: full, partial, and device-dependent complete row bands share the same reduction and spatial extent."
1301
+ }
1302
+ },
1303
+ {
1304
+ "name": "direct-1x1-float16-m64-k512-n4096",
1305
+ "preset": "model",
1306
+ "vars": {
1307
+ "batch": 1,
1308
+ "inChannels": 512,
1309
+ "outChannels": 64,
1310
+ "inH": 64,
1311
+ "inW": 64,
1312
+ "kernelH": 1,
1313
+ "kernelW": 1,
1314
+ "strideH": 1,
1315
+ "strideW": 1,
1316
+ "padH": 0,
1317
+ "padW": 0
1318
+ },
1319
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
1320
+ "inputs": {
1321
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float16", "dist": "normal", "seed": 354, "scale": 0.2 },
1322
+ "w": { "shape": [64, 512, 1, 1], "dtype": "float16", "dist": "normal", "seed": 355, "scale": 0.02 }
1323
+ },
1324
+ "outputs": { "y": { "shape": [1, 64, 64, 64], "dtype": "float16" } },
1325
+ "bench": {
1326
+ "metrics": [
1327
+ {
1328
+ "type": "gflops",
1329
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1330
+ }
1331
+ ]
1332
+ },
1333
+ "provenance": {
1334
+ "source": "synthetic",
1335
+ "notes": "Row-tile boundary sweep: full, partial, and device-dependent complete row bands share the same reduction and spatial extent."
1336
+ }
1337
+ },
1338
+ {
1339
+ "name": "direct-1x1-float16-m96-k512-n4096",
1340
+ "preset": "model",
1341
+ "vars": {
1342
+ "batch": 1,
1343
+ "inChannels": 512,
1344
+ "outChannels": 96,
1345
+ "inH": 64,
1346
+ "inW": 64,
1347
+ "kernelH": 1,
1348
+ "kernelW": 1,
1349
+ "strideH": 1,
1350
+ "strideW": 1,
1351
+ "padH": 0,
1352
+ "padW": 0
1353
+ },
1354
+ "attrs": { "activation": "Relu", "strides": [1, 1], "pads": [0, 0, 0, 0] },
1355
+ "inputs": {
1356
+ "x": { "shape": [1, 512, 64, 64], "dtype": "float16", "dist": "normal", "seed": 354, "scale": 0.2 },
1357
+ "w": { "shape": [96, 512, 1, 1], "dtype": "float16", "dist": "normal", "seed": 355, "scale": 0.02 }
1358
+ },
1359
+ "outputs": { "y": { "shape": [1, 96, 64, 64], "dtype": "float16" } },
1360
+ "bench": {
1361
+ "metrics": [
1362
+ {
1363
+ "type": "gflops",
1364
+ "value": "2 * args.batch * args.outChannels * dim(shapes.y, 2) * dim(shapes.y, 3) * dim(shapes.w, 1) * args.kernelH * args.kernelW"
1365
+ }
1366
+ ]
1367
+ },
1368
+ "provenance": {
1369
+ "source": "synthetic",
1370
+ "notes": "Row-tile boundary sweep: full, partial, and device-dependent complete row bands share the same reduction and spatial extent."
1371
+ }
1372
  }
1373
  ]
1374
  }
build/webgpu/conv-1x1-gemm-tiled-reg.wgsl.jinja CHANGED
@@ -1,11 +1,10 @@
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
  //
@@ -14,33 +13,35 @@
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
36
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
37
- // already +/-1 to full f32 precision by |v| ~ 9.
38
  return tanh(clamp(v, -10.0, 10.0));
39
- {% elif activation == "HardSigmoid" %}
40
- return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
41
  {% else %}
42
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
43
  {% endif %}
 
 
 
44
  }
45
  {% endif %}
46
 
@@ -67,19 +68,19 @@ const BN_VECS: u32 = BN / 4u;
67
  // implicitIm2col leaves B unmaterialized. While staging a tile, the kernel
68
  // decodes each (k, n) column-matrix coordinate into a raw NCHW input address.
69
  // Overlapping windows may reread input values, trading address arithmetic and
70
- // cache traffic for the storage and bandwidth of the expanded column matrix.
71
  {% set implicitIm2col = implicitIm2col is defined and implicitIm2col %}
72
  {% set fusedNarrowProjection = fusedNarrowProjection is defined and fusedNarrowProjection %}
73
- {% if fusedNarrowProjection %}
74
- {% set projectionChannels = projectionOutChannels %}
75
- {% set projectionInputAct = inputActivation %}
76
- {% set projectionOutputAct = outputActivation %}
77
- {% set projectionScaled = hasOutputScale %}
78
- {% set projectionBiased = hasProjectionBias %}
79
- {% endif %}
80
  {% set narrowProjectionTile = "tileB" if GEMM_BK >= GEMM_BM else "projectionTile" %}
81
  {% set splitKValue = splitK if splitK is defined else 1 %}
82
  {% set splitKPartial = splitKValue > 1 %}
 
 
 
 
83
  {% set gemmKLimit = "kEnd" if splitKPartial else "K" %}
84
  {% set implicitGatherMode = tunables.IMPLICIT_TILED_GATHER_MODE if tunables.IMPLICIT_TILED_GATHER_MODE is defined else 0 %}
85
  {% set implicitGatherWgSize = GEMM_WG_X * GEMM_WG_Y %}
@@ -105,11 +106,48 @@ const CONV_IN_W: u32 = {{ convInW }}u;
105
  const CONV_OUT_W: u32 = {{ convOutW }}u;
106
  const CONV_IN_CHANNELS: u32 = {{ convInChannels }}u;
107
  {% endif %}
 
 
 
 
 
 
 
 
 
 
108
 
109
  {% if fusedNarrowProjection %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
  const PROJECTION_OUT_C: u32 = {{ projectionChannels }}u;
112
- {% else %}{% set emitConvStoreOut = not splitKPartial %}{% if emitConvStoreOut | default(true) %}fn store_out(m: u32, n: u32, yBase: u32, raw: f32) {
 
113
  if (m >= params.M || n >= params.N) {
114
  return;
115
  }
@@ -201,7 +239,7 @@ var<workgroup> projectionTile: array<array<vec4<f32>, BN_VECS>, BM>;
201
  let bk = {{ rowBase }} + br;
202
  var bvec = vec4<{{ tileT }}>({{ tileT }}(0.0));
203
  if (bk < {{ rowLimit }}) {
204
- // k is the OIHW weight flattening (ic * KH + kh) * KW + kw; n is the output position.
205
  let ic = bk / CONV_KSIZE;
206
  let kq = bk % CONV_KSIZE;
207
  let kh = kq / CONV_KERNEL_W;
@@ -319,21 +357,41 @@ fn main(
319
  let N = params.N;
320
  let mBase = wg.y * BM;
321
  let nBase = wg.x * BN;
 
 
 
 
 
 
322
  let batch = wg.z;
 
323
  {% if not implicitIm2col %}
324
  let xBatchBase = batch * K * N;
325
  {% endif %}
326
  let li = lid.y * WG_X + lid.x;
327
 
328
- // Explicit vectors give the backend fixed register indices and expose TM
329
- // independent FMA chains.
330
  {% for row in range(GEMM_TM) %}
331
  var acc{{ row }} = vec{{ GEMM_TN }}<f32>(0.0);
332
  {% endfor %}
333
 
 
 
 
 
 
 
 
 
 
334
  let numTiles = (K + BK - 1u) / BK;
 
335
  for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
 
 
 
336
  let kBase = kt * BK;
 
337
  {{ load_a_vec4("mBase", "kBase", "M", gemmKLimit) }}
338
  {% if useCarriedImplicitGather %}
339
  {{ load_b_implicit_carried("kBase", "nBase", gemmKLimit, "N") }}
@@ -360,14 +418,22 @@ fn main(
360
  workgroupBarrier();
361
  }
362
 
 
 
 
 
 
363
  {% if not fusedNarrowProjection %}
364
  let yBatchBase = batch * M * N;
365
  {% endif %}
366
  let m0 = mBase + lid.y * TM;
367
  let n0 = nBase + lid.x * TN;
 
368
  {% for row in range(GEMM_TM) %}
369
  {% for column in range(GEMM_TN) %}
370
- {% if fusedNarrowProjection %}
 
 
371
  {{ 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]) }}
372
  {% else %}
373
  store_out(m0 + {{ row }}u, n0 + {{ column }}u, yBatchBase, acc{{ row }}.{{ components[column] }});
@@ -396,7 +462,7 @@ fn main(
396
 
397
  let projectionYBase = batch * PROJECTION_OUT_C * N;
398
  {% for oc in range(projectionChannels) %}
399
- let projected{{ oc }} = projectionAcc{{ oc }}{% if projectionBiased %} + vec4<f32>(f32(projectionBias[{{ oc }}u])){% endif %};
400
  {% endfor %}
401
  {% for column in range(4) %}
402
  let projectionN{{ column }} = projectionNBase + {{ column }}u;
@@ -409,11 +475,7 @@ fn main(
409
  {% else %}
410
  let activated{{ oc }}_{{ column }} = projected{{ oc }}.{{ components[column] }};
411
  {% endif %}
412
- {% if projectionScaled %}
413
- y[projectionYBase + {{ oc }}u * N + projectionN{{ column }}] = activated{{ oc }}_{{ column }} * params.outputScale;
414
- {% else %}
415
  y[projectionYBase + {{ oc }}u * N + projectionN{{ column }}] = activated{{ oc }}_{{ column }};
416
- {% endif %}
417
  {% endfor %}
418
  }
419
  {% endfor %}
 
1
+ // Register-blocked batched matrix product:
2
+ // C[b] (M x N) = A (M x K) * B[b] (K x N). Workgroup lanes compute TM x TN
3
+ // micro-tiles. The defaults use a 16x8 lane grid with an 8x4 micro-tile over a
4
+ // 64x64 output tile. A is shared across the batch; B and C use the dispatch.z
5
+ // batch offset. Accumulation is f32; an optional epilogue applies configured
6
+ // bias, residual, and activation terms in the accumulator domain on store.
7
+ // Every matrix dimension is bounds-checked.
 
8
  // f16 operands remain packed in workgroup memory and widen only at the FMA site;
9
  // the f32 accumulation order is retained while shared traffic is halved.
10
  //
 
13
  // N columns for one K. One micro-tile step therefore reads TM + 4 vector words
14
  // instead of 4 * (TM + TN) scalars, so a shared word feeds four times as many
15
  // FMAs and the K loop runs four accumulation steps per iteration.
 
 
 
16
  {{ env.wgsl.resourceDeclarations }}
17
  {% set hasActivation = hasActivation is defined and hasActivation %}
18
+ {% set activation = activation | default("") %}
19
+ {% set actAlpha = actAlpha | default(0.0) %}
20
+ {% set actBeta = actBeta | default(0.0) %}
21
  {% if hasActivation %}
22
  // Apply the fused activation in the f32 accumulator before the single output
23
  // cast, avoiding an intermediate convolution tensor.
24
+ {% macro fused_act_return(mode, alpha, beta) -%}
25
+ {% if mode == "Relu" %}
26
  return max(v, 0.0);
27
+ {% elif mode == "Clip" %}
28
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
29
+ {% elif mode == "LeakyRelu" %}
30
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
31
+ {% elif mode == "Sigmoid" %}
32
  return 1.0 / (1.0 + exp(-v));
33
+ {% elif mode == "Tanh" %}
34
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
35
+ // the builtin preserves that saturated result for larger accumulators.
 
36
  return tanh(clamp(v, -10.0, 10.0));
37
+ {% elif mode == "HardSigmoid" %}
38
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
39
  {% else %}
40
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
41
  {% endif %}
42
+ {%- endmacro -%}
43
+ fn fused_act(v: f32) -> f32 {
44
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
45
  }
46
  {% endif %}
47
 
 
68
  // implicitIm2col leaves B unmaterialized. While staging a tile, the kernel
69
  // decodes each (k, n) column-matrix coordinate into a raw NCHW input address.
70
  // Overlapping windows may reread input values, trading address arithmetic and
71
+ // repeated input loads for writing and rereading an expanded column matrix.
72
  {% set implicitIm2col = implicitIm2col is defined and implicitIm2col %}
73
  {% set fusedNarrowProjection = fusedNarrowProjection is defined and fusedNarrowProjection %}
74
+ {% set projectionChannels = projectionOutChannels | default(0) %}
75
+ {% set projectionInputAct = inputActivation | default("none") %}
76
+ {% set projectionOutputAct = outputActivation | default("none") %}
 
 
 
 
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 kTiles = kTiles | default(0) %}
81
+ {% set mPadded = mPadded | default(0) %}
82
+ {% set nPadded = nPadded | default(0) %}
83
+ {% set batchCount = batchCount | default(0) %}
84
  {% set gemmKLimit = "kEnd" if splitKPartial else "K" %}
85
  {% set implicitGatherMode = tunables.IMPLICIT_TILED_GATHER_MODE if tunables.IMPLICIT_TILED_GATHER_MODE is defined else 0 %}
86
  {% set implicitGatherWgSize = GEMM_WG_X * GEMM_WG_Y %}
 
106
  const CONV_OUT_W: u32 = {{ convOutW }}u;
107
  const CONV_IN_CHANNELS: u32 = {{ convInChannels }}u;
108
  {% endif %}
109
+ {% if splitKPartial %}
110
+ // Split-K publishes raw f32 partials. A second pass owns the bias, optional
111
+ // epilogue, output cast, and logical-shape store.
112
+ const SPLIT_K: u32 = {{ splitKValue }}u;
113
+ const K_TILES: u32 = {{ kTiles }}u;
114
+ const PARTIAL_ROWS: u32 = {{ mPadded }}u;
115
+ const PARTIAL_COLS: u32 = {{ nPadded }}u;
116
+ const PARTIAL_BATCH_STRIDE: u32 = PARTIAL_ROWS * PARTIAL_COLS;
117
+ const PARTIAL_SLICE_STRIDE: u32 = {{ batchCount }}u * PARTIAL_BATCH_STRIDE;
118
+ {% endif %}
119
 
120
  {% if fusedNarrowProjection %}
121
+ {% set inputActivation = inputActivation | default("none") %}
122
+ {% set outputActivation = outputActivation | default("none") %}
123
+ {% if inputActivation == "relu" or outputActivation == "relu" %}
124
+ fn is_nan_f32(value: f32) -> bool {
125
+ let bits = bitcast<u32>(value);
126
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
127
+ }
128
+
129
+ fn projection_relu(value: f32) -> f32 {
130
+ var out = max(value, 0.0);
131
+ if (is_nan_f32(value)) {
132
+ out = value;
133
+ }
134
+ return out;
135
+ }
136
+ {% endif %}
137
+ {% if outputActivation == "sigmoid" %}
138
+ fn sigmoid_safe(x: f32) -> f32 {
139
+ if (x >= 0.0) {
140
+ let z = exp(-x);
141
+ return 1.0 / (1.0 + z);
142
+ }
143
+ let z = exp(x);
144
+ return z / (1.0 + z);
145
+ }
146
+ {% endif %}
147
 
148
  const PROJECTION_OUT_C: u32 = {{ projectionChannels }}u;
149
+ {% else %}{% set emitConvStoreOut = not splitKPartial %}{% set hasZ = hasZ is defined and hasZ %}
150
+ {% if emitConvStoreOut | default(true) %}fn store_out(m: u32, n: u32, yBase: u32, raw: f32) {
151
  if (m >= params.M || n >= params.N) {
152
  return;
153
  }
 
239
  let bk = {{ rowBase }} + br;
240
  var bvec = vec4<{{ tileT }}>({{ tileT }}(0.0));
241
  if (bk < {{ rowLimit }}) {
242
+ // k flattens the input-channel and spatial-tap axes; n is the output position.
243
  let ic = bk / CONV_KSIZE;
244
  let kq = bk % CONV_KSIZE;
245
  let kh = kq / CONV_KERNEL_W;
 
357
  let N = params.N;
358
  let mBase = wg.y * BM;
359
  let nBase = wg.x * BN;
360
+ {% if splitKPartial %}
361
+ // z carries a K slice within a batch image, matching the subgroup-matrix
362
+ // Conv split-K layout and the shared reduction pass.
363
+ let slice = wg.z % SPLIT_K;
364
+ let batch = wg.z / SPLIT_K;
365
+ {% else %}
366
  let batch = wg.z;
367
+ {% endif %}
368
  {% if not implicitIm2col %}
369
  let xBatchBase = batch * K * N;
370
  {% endif %}
371
  let li = lid.y * WG_X + lid.x;
372
 
373
+ // The statically named accumulator vectors form TM independent FMA chains.
 
374
  {% for row in range(GEMM_TM) %}
375
  var acc{{ row }} = vec{{ GEMM_TN }}<f32>(0.0);
376
  {% endfor %}
377
 
378
+ {% if splitKPartial %}
379
+ // Partition whole BK-wide tiles by quotient boundaries. This keeps every
380
+ // slice within one tile of every other slice and creates no empty tail slice
381
+ // whenever K_TILES >= SPLIT_K (this route requires a stronger floor).
382
+ let kBegin = (slice * K_TILES / SPLIT_K) * BK;
383
+ let kEnd = min(((slice + 1u) * K_TILES / SPLIT_K) * BK, K);
384
+ let kCount = kEnd - kBegin;
385
+ let numTiles = (kCount + BK - 1u) / BK;
386
+ {% else %}
387
  let numTiles = (K + BK - 1u) / BK;
388
+ {% endif %}
389
  for (var kt: u32 = 0u; kt < numTiles; kt = kt + 1u) {
390
+ {% if splitKPartial %}
391
+ let kBase = kBegin + kt * BK;
392
+ {% else %}
393
  let kBase = kt * BK;
394
+ {% endif %}
395
  {{ load_a_vec4("mBase", "kBase", "M", gemmKLimit) }}
396
  {% if useCarriedImplicitGather %}
397
  {{ load_b_implicit_carried("kBase", "nBase", gemmKLimit, "N") }}
 
418
  workgroupBarrier();
419
  }
420
 
421
+ {% if splitKPartial %}
422
+ let m0 = mBase + lid.y * TM;
423
+ let n0 = nBase + lid.x * TN;
424
+ let partialBase = slice * PARTIAL_SLICE_STRIDE + batch * PARTIAL_BATCH_STRIDE;
425
+ {% else %}
426
  {% if not fusedNarrowProjection %}
427
  let yBatchBase = batch * M * N;
428
  {% endif %}
429
  let m0 = mBase + lid.y * TM;
430
  let n0 = nBase + lid.x * TN;
431
+ {% endif %}
432
  {% for row in range(GEMM_TM) %}
433
  {% for column in range(GEMM_TN) %}
434
+ {% if splitKPartial %}
435
+ y[partialBase + (m0 + {{ row }}u) * PARTIAL_COLS + n0 + {{ column }}u] = acc{{ row }}.{{ components[column] }};
436
+ {% elif fusedNarrowProjection %}
437
  {{ 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]) }}
438
  {% else %}
439
  store_out(m0 + {{ row }}u, n0 + {{ column }}u, yBatchBase, acc{{ row }}.{{ components[column] }});
 
462
 
463
  let projectionYBase = batch * PROJECTION_OUT_C * N;
464
  {% for oc in range(projectionChannels) %}
465
+ let projected{{ oc }} = projectionAcc{{ oc }};
466
  {% endfor %}
467
  {% for column in range(4) %}
468
  let projectionN{{ column }} = projectionNBase + {{ column }}u;
 
475
  {% else %}
476
  let activated{{ oc }}_{{ column }} = projected{{ oc }}.{{ components[column] }};
477
  {% endif %}
 
 
 
478
  y[projectionYBase + {{ oc }}u * N + projectionN{{ column }}] = activated{{ oc }}_{{ column }};
 
479
  {% endfor %}
480
  }
481
  {% endfor %}
build/webgpu/conv-1x1-gemm-tiled.wgsl.jinja CHANGED
@@ -1,46 +1,39 @@
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
36
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
37
- // already +/-1 to full f32 precision by |v| ~ 9.
38
  return tanh(clamp(v, -10.0, 10.0));
39
- {% elif activation == "HardSigmoid" %}
40
- return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
41
  {% else %}
42
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
43
  {% endif %}
 
 
 
44
  }
45
  {% endif %}
46
 
@@ -49,8 +42,9 @@ const BK: u32 = 16u;
49
  const BM: u32 = 32u;
50
  const BN: u32 = 32u;
51
 
52
- // Store one output element with the optional bias/residual/activation epilogue in the
53
- // f32 accumulator domain: Y = activation(conv + bias + Z).
 
54
  fn store_out(m: u32, n: u32, yBase: u32, raw: f32) {
55
  if (m >= params.M || n >= params.N) {
56
  return;
 
1
+ // Tiled batched matrix product C[b] (M x N) = A (M x K) * B[b] (K x N).
2
+ // A 16x16 workgroup computes a 32x32 output tile with a 2x2 micro-tile per
3
+ // thread and a 16-element K tile. A is shared across the batch; B and C use the
4
+ // dispatch.z batch offset. Accumulation is f32 and is cast to the output type on
5
+ // store. A configured epilogue may add bias, residual, and activation terms.
6
+ // f16 operands remain packed in workgroup memory and widen when consumed. All
7
+ // M, N, and K accesses are bounds-checked.
 
 
 
 
 
 
 
 
 
 
 
 
8
  {{ env.wgsl.resourceDeclarations }}
9
  {% set hasActivation = hasActivation is defined and hasActivation %}
10
+ {% set activation = activation | default("") %}
11
+ {% set actAlpha = actAlpha | default(0.0) %}
12
+ {% set actBeta = actBeta | default(0.0) %}
13
  {% if hasActivation %}
14
  // Apply the fused activation in the f32 accumulator before the single output
15
  // cast, avoiding an intermediate convolution tensor.
16
+ {% macro fused_act_return(mode, alpha, beta) -%}
17
+ {% if mode == "Relu" %}
18
  return max(v, 0.0);
19
+ {% elif mode == "Clip" %}
20
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
21
+ {% elif mode == "LeakyRelu" %}
22
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
23
+ {% elif mode == "Sigmoid" %}
24
  return 1.0 / (1.0 + exp(-v));
25
+ {% elif mode == "Tanh" %}
26
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
27
+ // the builtin preserves that saturated result for larger accumulators.
 
28
  return tanh(clamp(v, -10.0, 10.0));
29
+ {% elif mode == "HardSigmoid" %}
30
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
31
  {% else %}
32
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
33
  {% endif %}
34
+ {%- endmacro -%}
35
+ fn fused_act(v: f32) -> f32 {
36
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
37
  }
38
  {% endif %}
39
 
 
42
  const BM: u32 = 32u;
43
  const BN: u32 = 32u;
44
 
45
+ // Store one output element with the configured bias/residual/activation
46
+ // epilogue in the f32 accumulator domain.
47
+ {% set hasZ = hasZ is defined and hasZ %}
48
  fn store_out(m: u32, n: u32, yBase: u32, raw: f32) {
49
  if (m >= params.M || n >= params.N) {
50
  return;
build/webgpu/conv-1x1-subgroup-matrix.wgsl.jinja CHANGED
@@ -1,6 +1,7 @@
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
@@ -10,15 +11,41 @@
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;
@@ -26,7 +53,35 @@ enable subgroup_size_control;
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" %}
@@ -43,29 +98,78 @@ diagnostic(off, chromium.subgroup_matrix_uniformity);
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;
@@ -91,6 +195,126 @@ fn loadSHMA(tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
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" %}
@@ -99,31 +323,38 @@ fn loadSHMB(b_base: u32, tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
119
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
120
- // already +/-1 to full f32 precision by |v| ~ 9.
121
  return tanh(clamp(v, -10.0, 10.0));
122
- {% elif activation == "HardSigmoid" %}
123
- return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
124
  {% else %}
125
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
126
  {% endif %}
 
 
 
127
  }
128
  {% endif %}
129
 
@@ -191,10 +422,18 @@ fn main(
191
  {% endif %}
192
  @builtin(subgroup_size) sg_size: u32
193
  ) {
 
 
 
 
 
 
 
194
  let batch = workgroup_id.z;
195
  let b_base = batch * B_BATCH_STRIDE;
196
  {% if not fusedNarrowProjection %}
197
  let c_base = batch * C_BATCH_STRIDE;
 
198
  {% endif %}
199
  let a_global_base = workgroup_id.y * TILE_ROWS;
200
  let b_global_base = workgroup_id.x * TILE_COLS;
@@ -213,21 +452,30 @@ fn main(
213
  var matC12: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
214
  var matC13: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
215
 
 
 
 
 
 
216
  for (var kidx = 0u; kidx < {{ kLoopVar }}; kidx = kidx + TILE_K) {
 
 
217
  loadSHMA(a_global_base, kidx, local_idx / 4u, local_idx % 4u);
218
  loadSHMB(b_base, b_global_base, kidx, local_idx / {{ bKChunks }}u, local_idx % {{ bKChunks }}u);
219
  workgroupBarrier();
 
220
 
221
  for (var step = 0u; step < TILE_K; step = step + 8u) {
222
- let matrix_a_offset = subtile_idy * SUB_ROWS * TILE_K + step;
223
- var matA0: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset, TILE_K);
224
- var matA1: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&tile_A, matrix_a_offset + 8u * TILE_K, TILE_K);
 
 
225
 
226
- let matrix_b_offset = subtile_idx * SUB_COLS * TILE_K + step;
227
- var matB0: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset, TILE_K);
228
- var matB1: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset + 8u * TILE_K, TILE_K);
229
- var matB2: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset + 16u * TILE_K, TILE_K);
230
- var matB3: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, col_major>(&tile_B, matrix_b_offset + 24u * TILE_K, TILE_K);
231
 
232
  matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
233
  matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
@@ -238,7 +486,9 @@ fn main(
238
  matC12 = subgroupMatrixMultiplyAccumulate(matA1, matB2, matC12);
239
  matC13 = subgroupMatrixMultiplyAccumulate(matA1, matB3, matC13);
240
  }
 
241
  workgroupBarrier();
 
242
  }
243
 
244
  {% if fusedNarrowProjection %}
@@ -268,7 +518,11 @@ fn main(
268
  {% endfor %}
269
  for (var channel = 0u; channel < M; channel = channel + 1u) {
270
  let producer_value = tile_B[channel * TILE_COLS + local_idx]{% if hasBias %} + f32(bias[channel]){% endif %};
 
 
 
271
  let activated_value = producer_value;
 
272
  {% for oc in range(projectionOutChannels) %}
273
  projected_acc{{ oc }} = projected_acc{{ oc }} + activated_value * f32(projectionW[{{ oc }}u * M + channel]);
274
  {% endfor %}
@@ -276,11 +530,30 @@ fn main(
276
 
277
  let fused_y_base = batch * {{ projectionOutChannels }}u * N + fused_global_col;
278
  {% for oc in range(projectionOutChannels) %}
279
- let projected{{ oc }} = projected_acc{{ oc }};
 
 
 
 
 
280
  let activated{{ oc }} = projected{{ oc }};
281
- y[fused_y_base + {{ oc }}u * N] = activated{{ oc }};
 
282
  {% endfor %}
283
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  {% elif useDirectMatrixStore %}
285
  {% if padded %}
286
  // Padded N only needs guarded publication for the final partial-width tile.
 
1
+ // Batched subgroup-matrix product C[b] (M x N) = A (M x K) * B[b] (K x N).
2
+ // A is row-major; B may be a direct matrix view, a materialized column matrix,
3
+ // or an on-demand gather. Accumulation is f32. A configured epilogue may apply
4
+ // bias or other output transforms after the full K reduction.
5
  //
6
  // Full 8x8 matrix loads require complete K/N tiles. The aligned path enforces
7
  // that geometry; `padded` zero-fills tails, and `implicitIm2col` gathers and
 
11
  // sums to tile-padded [splitK,batch,M_PAD,N_PAD] scratch; a later pass combines
12
  // the slices and applies bias or an epilogue. Padded tail cells remain zero and
13
  // are never copied to the logical output.
14
+ {% set directMatrixInputs = directMatrixInputs is defined and directMatrixInputs %}
15
  {% set fusedNarrowProjection = fusedNarrowProjection if fusedNarrowProjection is defined else false %}
16
  {% set polyphase = polyphaseConvTranspose is defined and polyphaseConvTranspose %}
17
+ {% set implicit = implicitIm2col is defined and implicitIm2col %}
18
+ {% set implicit3d = implicit and (convSpatialDims is defined and convSpatialDims == 3) %}
19
  {% set splitKValue = splitK if splitK is defined else 1 %}
20
  {% set splitKPartial = splitKValue > 1 %}
21
  {% set kLoopVar = "K_LOOP" if padded else "K" %}
22
  {% set nColsVar = "N_COLS" if padded else "N" %}
23
+ {% set hasZ = hasZ is defined and hasZ %}
24
+ {% set projectionOutChannels = projectionOutChannels | default(0) %}
25
+ {% set hasProjectionBias = hasProjectionBias is defined and hasProjectionBias %}
26
+ {% set hasOutputScale = hasOutputScale is defined and hasOutputScale %}
27
+ {% set convKernelH = convKernelH | default(0) %}
28
+ {% set convKernelW = convKernelW | default(0) %}
29
+ {% set convStrideH = convStrideH | default(0) %}
30
+ {% set convStrideW = convStrideW | default(0) %}
31
+ {% set convDilationH = convDilationH | default(0) %}
32
+ {% set convDilationW = convDilationW | default(0) %}
33
+ {% set convPadTop = convPadTop | default(0) %}
34
+ {% set convPadLeft = convPadLeft | default(0) %}
35
+ {% set convInH = convInH | default(0) %}
36
+ {% set convInW = convInW | default(0) %}
37
+ {% set convOutW = convOutW | default(0) %}
38
+ {% set convInChannels = convInChannels | default(0) %}
39
+ {% set convKernelD = convKernelD | default(0) %}
40
+ {% set convStrideD = convStrideD | default(0) %}
41
+ {% set convDilationD = convDilationD | default(0) %}
42
+ {% set convPadFront = convPadFront | default(0) %}
43
+ {% set convInD = convInD | default(0) %}
44
+ {% set convOutH = convOutH | default(0) %}
45
+ {% set nPadded = nPadded | default(0) %}
46
+ {% set mPadded = mPadded | default(0) %}
47
+ {% set kChunk = kChunk | default(0) %}
48
+ {% set batchCount = batchCount | default(0) %}
49
  enable subgroups;
50
  {% if pinSubgroupSize32 %}
51
  enable subgroup_size_control;
 
53
  enable chromium_experimental_subgroup_matrix;
54
  diagnostic(off, chromium.subgroup_matrix_uniformity);
55
 
56
+
57
  {{ env.wgsl.resourceDeclarations }}
58
+ {% if fusedNarrowProjection %}{% set inputActivation = inputActivation | default("none") %}
59
+ {% set outputActivation = outputActivation | default("none") %}
60
+ {% if inputActivation == "relu" or outputActivation == "relu" %}
61
+ fn is_nan_f32(value: f32) -> bool {
62
+ let bits = bitcast<u32>(value);
63
+ return (bits & 0x7f800000u) == 0x7f800000u && (bits & 0x007fffffu) != 0u;
64
+ }
65
+
66
+ fn projection_relu(value: f32) -> f32 {
67
+ var out = max(value, 0.0);
68
+ if (is_nan_f32(value)) {
69
+ out = value;
70
+ }
71
+ return out;
72
+ }
73
+ {% endif %}
74
+ {% if outputActivation == "sigmoid" %}
75
+ fn sigmoid_safe(x: f32) -> f32 {
76
+ if (x >= 0.0) {
77
+ let z = exp(-x);
78
+ return 1.0 / (1.0 + z);
79
+ }
80
+ let z = exp(x);
81
+ return z / (1.0 + z);
82
+ }
83
+ {% endif %}
84
+ {% endif %}
85
 
86
  {% set operandScalar = fScalar %}
87
  {% set accScalar = "f32" %}
 
98
 
99
  const M: u32 = {{ M }}u;
100
  const K: u32 = {{ K }}u;
101
+ {% if not (splitKPartial and padded) or implicit %}
102
  const N: u32 = {{ N }}u;
103
+ {% endif %}
104
  {% if padded %}
105
  const K_LOOP: u32 = {{ kPadded }}u;
106
  {% endif %}
107
+ {% if implicit %}
108
+ const CONV_KERNEL_H: u32 = {{ convKernelH }}u;
109
+ const CONV_KERNEL_W: u32 = {{ convKernelW }}u;
110
+ {% if implicit3d %}
111
+ const CONV_KERNEL_D: u32 = {{ convKernelD }}u;
112
+ const CONV_KSIZE_HW: u32 = CONV_KERNEL_H * CONV_KERNEL_W;
113
+ const CONV_KSIZE: u32 = CONV_KERNEL_D * CONV_KSIZE_HW;
114
+ const CONV_STRIDE_D: u32 = {{ convStrideD }}u;
115
+ const CONV_DILATION_D: u32 = {{ convDilationD }}u;
116
+ const CONV_PAD_FRONT: i32 = {{ convPadFront }};
117
+ const CONV_IN_D: i32 = {{ convInD }};
118
+ const CONV_OUT_H: u32 = {{ convOutH }}u;
119
+ {% else %}
120
+ const CONV_KSIZE: u32 = CONV_KERNEL_H * CONV_KERNEL_W;
121
+ {% endif %}
122
+ const CONV_STRIDE_H: u32 = {{ convStrideH }}u;
123
+ const CONV_STRIDE_W: u32 = {{ convStrideW }}u;
124
+ const CONV_DILATION_H: u32 = {{ convDilationH }}u;
125
+ const CONV_DILATION_W: u32 = {{ convDilationW }}u;
126
+ const CONV_PAD_TOP: i32 = {{ convPadTop }};
127
+ const CONV_PAD_LEFT: i32 = {{ convPadLeft }};
128
+ const CONV_IN_H: i32 = {{ convInH }};
129
+ const CONV_IN_W: i32 = {{ convInW }};
130
+ const CONV_OUT_W: u32 = {{ convOutW }}u;
131
+ {% if implicit3d %}
132
+ // X is [batch, inChannels, inD, inH, inW]; the tile gather indexes it directly.
133
+ const B_BATCH_STRIDE: u32 = {{ convInChannels }}u * u32(CONV_IN_D) * u32(CONV_IN_H) * u32(CONV_IN_W);
134
+ {% else %}
135
+ // X is [batch, inChannels, inH, inW]; the tile gather indexes it directly.
136
+ const B_BATCH_STRIDE: u32 = {{ convInChannels }}u * u32(CONV_IN_H) * u32(CONV_IN_W);
137
+ {% endif %}
138
+ {% else %}
139
  {% if padded %}
140
  const N_COLS: u32 = {{ nPadded }}u;
141
  {% endif %}
142
  const B_BATCH_STRIDE: u32 = {{ kLoopVar }} * {{ nColsVar }};
143
+ {% endif %}
144
  {% if not splitKPartial and not polyphase and not fusedNarrowProjection %}
145
  const C_BATCH_STRIDE: u32 = M * N;
146
  {% endif %}
147
+ {% if splitKPartial %}
148
+ const SPLIT_K: u32 = {{ splitKValue }}u;
149
+ // Whole 32-wide K tiles per slice, rounded up, with the last slice clamped to
150
+ // K_LOOP. This avoids padding K to a multiple of SPLIT_K times the tile width.
151
+ // A slice entirely past K_LOOP runs zero iterations and stores zero
152
+ // accumulators, which is the correct partial for an empty K range.
153
+ const K_CHUNK: u32 = {{ kChunk }}u;
154
+ const PARTIAL_ROWS: u32 = {{ mPadded }}u;
155
+ const PARTIAL_COLS: u32 = {{ nPadded }}u;
156
+ const PARTIAL_SLICE_STRIDE: u32 = {{ batchCount }}u * PARTIAL_ROWS * PARTIAL_COLS;
157
+ {% endif %}
158
  const TILE_COLS: u32 = {{ tileColsValue }}u;
159
  const TILE_ROWS: u32 = {{ tileRowsValue }}u;
160
  const TILE_K: u32 = 32u;
161
  const SUB_COLS: u32 = {{ subColsValue }}u;
162
  const SUB_ROWS: u32 = {{ subRowsValue }}u;
163
 
164
+ {% if not directMatrixInputs %}
165
  var<workgroup> tile_A: array<{{ operandScalar }}, {{ tileRowsValue }} * 32>;
166
  var<workgroup> tile_B: array<{{ operandScalar }}, {{ tileColsValue }} * 32>;
167
+ {% endif %}
168
  {% if (not useDirectMatrixStore or padded) and not splitKPartial and not fusedNarrowProjection %}
169
  var<workgroup> scratch: array<array<array<{{ accScalar }}, 64>, 4>, {{ (workgroupThreadsValue / 32)|int }}>;
170
 
171
  {% endif %}
172
+ {% if not directMatrixInputs %}
173
  fn loadSHMA(tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
174
  let a_global = tile_base + row;
175
  let col = c_idx * 8u;
 
195
  fn loadSHMB(b_base: u32, tile_base: u32, k_idx: u32, row: u32, c_idx: u32) {
196
  let b_col = tile_base + row;
197
  let col = c_idx * {{ bLoadWidth }}u;
198
+ {% if implicit %}
199
+ // One output position per tile column, so its window origin is loop-invariant.
200
+ {% if implicit3d %}
201
+ let id0 = i32((b_col / (CONV_OUT_W * CONV_OUT_H)) * CONV_STRIDE_D) - CONV_PAD_FRONT;
202
+ let ih0 = i32(((b_col / CONV_OUT_W) % CONV_OUT_H) * CONV_STRIDE_H) - CONV_PAD_TOP;
203
+ {% else %}
204
+ let ih0 = i32((b_col / CONV_OUT_W) * CONV_STRIDE_H) - CONV_PAD_TOP;
205
+ {% endif %}
206
+ let iw0 = i32((b_col % CONV_OUT_W) * CONV_STRIDE_W) - CONV_PAD_LEFT;
207
+ let in_column = b_col < N;
208
+ // k advances by exactly one per element, so the (in-channel, tap-row, tap-col)
209
+ // decomposition is CARRIED across the loop instead of recomputed: two divisions
210
+ // and two moduli per loaded element become one increment and one compare, with
211
+ // the source coordinates advanced by the dilation in step. Only the entry values
212
+ // divide, and their divisors are compile-time literals.
213
+ var k = k_idx + col;
214
+ var ic = k / CONV_KSIZE;
215
+ var kq = k % CONV_KSIZE;
216
+ {% if implicit3d %}
217
+ var kd = kq / CONV_KSIZE_HW;
218
+ var khw = kq % CONV_KSIZE_HW;
219
+ var kh = khw / CONV_KERNEL_W;
220
+ var kw = khw % CONV_KERNEL_W;
221
+ var id = id0 + i32(kd * CONV_DILATION_D);
222
+ {% else %}
223
+ var kh = kq / CONV_KERNEL_W;
224
+ var kw = kq % CONV_KERNEL_W;
225
+ {% endif %}
226
+ var ih = ih0 + i32(kh * CONV_DILATION_H);
227
+ var iw = iw0 + i32(kw * CONV_DILATION_W);
228
+ // A tile column whose whole kernel window and K span are in bounds cannot
229
+ // take any of the element guards below. Hoist them into one uniform test so
230
+ // the interior loop carries a plain address instead of recomputing coordinates.
231
+ let interior = in_column
232
+ && k_idx + col + {{ bLoadWidth }}u <= K
233
+ {% if implicit3d %}
234
+ && id0 >= 0 && id0 + i32((CONV_KERNEL_D - 1u) * CONV_DILATION_D) < CONV_IN_D
235
+ {% endif %}
236
+ && ih0 >= 0 && ih0 + i32((CONV_KERNEL_H - 1u) * CONV_DILATION_H) < CONV_IN_H
237
+ && iw0 >= 0 && iw0 + i32((CONV_KERNEL_W - 1u) * CONV_DILATION_W) < CONV_IN_W;
238
+ if (interior) {
239
+ let colStep = i32(CONV_DILATION_W);
240
+ let rowStep = i32(CONV_DILATION_H) * CONV_IN_W;
241
+ {% if implicit3d %}
242
+ let depthStep = i32(CONV_DILATION_D) * CONV_IN_H * CONV_IN_W;
243
+ let planeStep = CONV_IN_D * CONV_IN_H * CONV_IN_W;
244
+ {% else %}
245
+ let planeStep = CONV_IN_H * CONV_IN_W;
246
+ {% endif %}
247
+ // Advancing k already added one colStep, so each wrap rewinds that plus the taps it walked.
248
+ let kwWrap = colStep * i32(CONV_KERNEL_W) - rowStep;
249
+ {% if implicit3d %}
250
+ let khWrap = rowStep * i32(CONV_KERNEL_H) - depthStep;
251
+ let kdWrap = depthStep * i32(CONV_KERNEL_D) - planeStep;
252
+ var addr = i32(b_base) + i32(ic) * planeStep + (id * CONV_IN_H + ih) * CONV_IN_W + iw;
253
+ {% else %}
254
+ let khWrap = rowStep * i32(CONV_KERNEL_H) - planeStep;
255
+ var addr = i32(b_base) + i32(ic) * planeStep + ih * CONV_IN_W + iw;
256
+ {% endif %}
257
+ for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
258
+ tile_B[row * TILE_K + col + i] = {{ operandScalar }}(xm[u32(addr)]);
259
+ addr = addr + colStep;
260
+ kw = kw + 1u;
261
+ if (kw == CONV_KERNEL_W) {
262
+ kw = 0u;
263
+ addr = addr - kwWrap;
264
+ kh = kh + 1u;
265
+ if (kh == CONV_KERNEL_H) {
266
+ kh = 0u;
267
+ addr = addr - khWrap;
268
+ {% if implicit3d %}
269
+ kd = kd + 1u;
270
+ if (kd == CONV_KERNEL_D) {
271
+ kd = 0u;
272
+ addr = addr - kdWrap;
273
+ }
274
+ {% endif %}
275
+ }
276
+ }
277
+ }
278
+ return;
279
+ }
280
+ for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
281
+ var value = {{ operandScalar }}(0.0);
282
+ // The K and N tails read as exact zeros, exactly as the materialized padded
283
+ // cols buffer does, so they contribute nothing to the dot product.
284
+ if (in_column && k < K{% if implicit3d %} && id >= 0 && id < CONV_IN_D{% endif %} && ih >= 0 && ih < CONV_IN_H && iw >= 0 && iw < CONV_IN_W) {
285
+ {% if implicit3d %}
286
+ value = {{ operandScalar }}(xm[b_base + ((ic * u32(CONV_IN_D) + u32(id)) * u32(CONV_IN_H) + u32(ih)) * u32(CONV_IN_W) + u32(iw)]);
287
+ {% else %}
288
+ value = {{ operandScalar }}(xm[b_base + (ic * u32(CONV_IN_H) + u32(ih)) * u32(CONV_IN_W) + u32(iw)]);
289
+ {% endif %}
290
+ }
291
+ tile_B[row * TILE_K + col + i] = value;
292
+ k = k + 1u;
293
+ kw = kw + 1u;
294
+ iw = iw + i32(CONV_DILATION_W);
295
+ if (kw == CONV_KERNEL_W) {
296
+ kw = 0u;
297
+ iw = iw0;
298
+ kh = kh + 1u;
299
+ ih = ih + i32(CONV_DILATION_H);
300
+ if (kh == CONV_KERNEL_H) {
301
+ kh = 0u;
302
+ ih = ih0;
303
+ {% if implicit3d %}
304
+ kd = kd + 1u;
305
+ id = id + i32(CONV_DILATION_D);
306
+ if (kd == CONV_KERNEL_D) {
307
+ kd = 0u;
308
+ id = id0;
309
+ ic = ic + 1u;
310
+ }
311
+ {% else %}
312
+ ic = ic + 1u;
313
+ {% endif %}
314
+ }
315
+ }
316
+ }
317
+ {% else %}
318
  for (var i = 0u; i < {{ bLoadWidth }}u; i = i + 1u) {
319
  let k = k_idx + col + i;
320
  {% if operandScalar == "f16" %}
 
323
  tile_B[row * TILE_K + col + i] = f32(xm[b_base + k * {{ nColsVar }} + b_col]);
324
  {% endif %}
325
  }
326
+ {% endif %}
327
  }
328
 
329
+ {% endif %}
330
  {% set hasActivation = hasActivation is defined and hasActivation %}
331
+ {% set activation = activation | default("") %}
332
+ {% set actAlpha = actAlpha | default(0.0) %}
333
+ {% set actBeta = actBeta | default(0.0) %}
334
  {% if hasActivation %}
335
  // Apply the fused activation in the f32 accumulator before the single output
336
  // cast, avoiding an intermediate convolution tensor.
337
+ {% macro fused_act_return(mode, alpha, beta) -%}
338
+ {% if mode == "Relu" %}
339
  return max(v, 0.0);
340
+ {% elif mode == "Clip" %}
341
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
342
+ {% elif mode == "LeakyRelu" %}
343
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
344
+ {% elif mode == "Sigmoid" %}
345
  return 1.0 / (1.0 + exp(-v));
346
+ {% elif mode == "Tanh" %}
347
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
348
+ // the builtin preserves that saturated result for larger accumulators.
 
349
  return tanh(clamp(v, -10.0, 10.0));
350
+ {% elif mode == "HardSigmoid" %}
351
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
352
  {% else %}
353
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
354
  {% endif %}
355
+ {%- endmacro -%}
356
+ fn fused_act(v: f32) -> f32 {
357
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
358
  }
359
  {% endif %}
360
 
 
422
  {% endif %}
423
  @builtin(subgroup_size) sg_size: u32
424
  ) {
425
+ {% if splitKPartial %}
426
+ // z carries both axes, with split slices contiguous inside each batch image.
427
+ let slice = workgroup_id.z % SPLIT_K;
428
+ let batch = workgroup_id.z / SPLIT_K;
429
+ let b_base = batch * B_BATCH_STRIDE;
430
+ let c_base = slice * PARTIAL_SLICE_STRIDE + batch * PARTIAL_ROWS * PARTIAL_COLS;
431
+ {% else %}
432
  let batch = workgroup_id.z;
433
  let b_base = batch * B_BATCH_STRIDE;
434
  {% if not fusedNarrowProjection %}
435
  let c_base = batch * C_BATCH_STRIDE;
436
+ {% endif %}
437
  {% endif %}
438
  let a_global_base = workgroup_id.y * TILE_ROWS;
439
  let b_global_base = workgroup_id.x * TILE_COLS;
 
452
  var matC12: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
453
  var matC13: subgroup_matrix_result<{{ accScalar }}, 8, 8>;
454
 
455
+ {% if splitKPartial %}
456
+ let k_begin = slice * K_CHUNK;
457
+ let k_end = min(k_begin + K_CHUNK, {{ kLoopVar }});
458
+ for (var kidx = k_begin; kidx < k_end; kidx = kidx + TILE_K) {
459
+ {% else %}
460
  for (var kidx = 0u; kidx < {{ kLoopVar }}; kidx = kidx + TILE_K) {
461
+ {% endif %}
462
+ {% if not directMatrixInputs %}
463
  loadSHMA(a_global_base, kidx, local_idx / 4u, local_idx % 4u);
464
  loadSHMB(b_base, b_global_base, kidx, local_idx / {{ bKChunks }}u, local_idx % {{ bKChunks }}u);
465
  workgroupBarrier();
466
+ {% endif %}
467
 
468
  for (var step = 0u; step < TILE_K; step = step + 8u) {
469
+ {% set directInputs = directMatrixInputs is defined and directMatrixInputs %}
470
+ let matrix_a_offset = {% if directInputs %}(a_global_base + subtile_idy * SUB_ROWS) * K + kidx + step{% else %}subtile_idy * SUB_ROWS * TILE_K + step{% endif %};
471
+ {% for r in range(2) %}
472
+ var matA{{ r }}: subgroup_matrix_left<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_left<{{ operandScalar }}, 8, 8>, row_major>(&{{ "w" if directInputs else "tile_A" }}, matrix_a_offset{% if r > 0 %} + 8u * {{ "K" if directInputs else "TILE_K" }}{% endif %}, {{ "K" if directInputs else "TILE_K" }});
473
+ {% endfor %}
474
 
475
+ let matrix_b_offset = {% if directInputs %}b_base + (kidx + step) * N + b_global_base + subtile_idx * SUB_COLS{% else %}subtile_idx * SUB_COLS * TILE_K + step{% endif %};
476
+ {% for c in range(4) %}
477
+ var matB{{ c }}: subgroup_matrix_right<{{ operandScalar }}, 8, 8> = subgroupMatrixLoad<subgroup_matrix_right<{{ operandScalar }}, 8, 8>, {% if directInputs %}row_major{% else %}col_major{% endif %}>(&{{ "xm" if directInputs else "tile_B" }}, matrix_b_offset{% if c > 0 %} + {{ c * 8 }}u{% if not directInputs %} * TILE_K{% endif %}{% endif %}, {{ "N" if directInputs else "TILE_K" }});
478
+ {% endfor %}
 
479
 
480
  matC00 = subgroupMatrixMultiplyAccumulate(matA0, matB0, matC00);
481
  matC01 = subgroupMatrixMultiplyAccumulate(matA0, matB1, matC01);
 
486
  matC12 = subgroupMatrixMultiplyAccumulate(matA1, matB2, matC12);
487
  matC13 = subgroupMatrixMultiplyAccumulate(matA1, matB3, matC13);
488
  }
489
+ {% if not directMatrixInputs %}
490
  workgroupBarrier();
491
+ {% endif %}
492
  }
493
 
494
  {% if fusedNarrowProjection %}
 
518
  {% endfor %}
519
  for (var channel = 0u; channel < M; channel = channel + 1u) {
520
  let producer_value = tile_B[channel * TILE_COLS + local_idx]{% if hasBias %} + f32(bias[channel]){% endif %};
521
+ {% if inputActivation == "relu" %}
522
+ let activated_value = projection_relu(producer_value);
523
+ {% else %}
524
  let activated_value = producer_value;
525
+ {% endif %}
526
  {% for oc in range(projectionOutChannels) %}
527
  projected_acc{{ oc }} = projected_acc{{ oc }} + activated_value * f32(projectionW[{{ oc }}u * M + channel]);
528
  {% endfor %}
 
530
 
531
  let fused_y_base = batch * {{ projectionOutChannels }}u * N + fused_global_col;
532
  {% for oc in range(projectionOutChannels) %}
533
+ let projected{{ oc }} = projected_acc{{ oc }}{% if hasProjectionBias %} + f32(projectionBias[{{ oc }}u]){% endif %};
534
+ {% if outputActivation == "relu" %}
535
+ let activated{{ oc }} = projection_relu(projected{{ oc }});
536
+ {% elif outputActivation == "sigmoid" %}
537
+ let activated{{ oc }} = sigmoid_safe(projected{{ oc }});
538
+ {% else %}
539
  let activated{{ oc }} = projected{{ oc }};
540
+ {% endif %}
541
+ y[fused_y_base + {{ oc }}u * N] = activated{{ oc }}{% if hasOutputScale %} * params.outputScale{% endif %};
542
  {% endfor %}
543
  }
544
+ {% elif splitKPartial %}
545
+ // Raw partials, no bias and no epilogue — the reduce pass owns both. The
546
+ // scratch is whole tiles in both axes, so this needs none of the column or
547
+ // row guards the real output store carries.
548
+ let partial_offset = c_base + (a_global_base + base_A) * PARTIAL_COLS + b_global_base + base_B;
549
+ subgroupMatrixStore<row_major>(&y, partial_offset + 0u * PARTIAL_COLS + 0u, matC00, PARTIAL_COLS);
550
+ subgroupMatrixStore<row_major>(&y, partial_offset + 0u * PARTIAL_COLS + 8u, matC01, PARTIAL_COLS);
551
+ subgroupMatrixStore<row_major>(&y, partial_offset + 0u * PARTIAL_COLS + 16u, matC02, PARTIAL_COLS);
552
+ subgroupMatrixStore<row_major>(&y, partial_offset + 0u * PARTIAL_COLS + 24u, matC03, PARTIAL_COLS);
553
+ subgroupMatrixStore<row_major>(&y, partial_offset + 8u * PARTIAL_COLS + 0u, matC10, PARTIAL_COLS);
554
+ subgroupMatrixStore<row_major>(&y, partial_offset + 8u * PARTIAL_COLS + 8u, matC11, PARTIAL_COLS);
555
+ subgroupMatrixStore<row_major>(&y, partial_offset + 8u * PARTIAL_COLS + 16u, matC12, PARTIAL_COLS);
556
+ subgroupMatrixStore<row_major>(&y, partial_offset + 8u * PARTIAL_COLS + 24u, matC13, PARTIAL_COLS);
557
  {% elif useDirectMatrixStore %}
558
  {% if padded %}
559
  // Padded N only needs guarded publication for the final partial-width tile.
build/webgpu/conv-direct-nd.wgsl.jinja CHANGED
@@ -1,42 +1,47 @@
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
23
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
24
- // already +/-1 to full f32 precision by |v| ~ 9.
25
  return tanh(clamp(v, -10.0, 10.0));
26
- {% elif activation == "HardSigmoid" %}
27
- return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
28
  {% else %}
29
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
30
  {% endif %}
 
 
 
31
  }
32
  {% endif %}
33
 
 
34
  const WG: u32 = {{ convWorkgroupSize }}u;
35
 
36
  @compute @workgroup_size(WG)
37
- fn main(@builtin(global_invocation_id) gid: vec3<u32>,
38
- @builtin(num_workgroups) nwg: vec3<u32>) {
39
- let index = gid.x + gid.y * nwg.x * WG;
40
  if (index >= params.count) {
41
  return;
42
  }
@@ -52,7 +57,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>,
52
  let group = oc / params.outChannelsPerGroup;
53
 
54
  var acc = 0.0;
55
- {% if source.hasBias %}
56
  acc = f32(bias[oc]);
57
  {% endif %}
58
  for (var ic = 0u; ic < params.weightInChannels; ic += 1u) {
 
1
+ // Direct N-dimensional 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 usesF16Spec %}
5
  enable f16;
6
  {% endif %}
7
  {{ env.wgsl.resourceDeclarations }}
8
  {% set hasActivation = hasActivation is defined and hasActivation %}
9
+ {% set activation = activation | default("") %}
10
+ {% set actAlpha = actAlpha | default(0.0) %}
11
+ {% set actBeta = actBeta | default(0.0) %}
12
  {% if hasActivation %}
13
  // Apply the fused activation in the f32 accumulator before the single output
14
  // cast, avoiding an intermediate convolution tensor.
15
+ {% macro fused_act_return(mode, alpha, beta) -%}
16
+ {% if mode == "Relu" %}
17
  return max(v, 0.0);
18
+ {% elif mode == "Clip" %}
19
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
20
+ {% elif mode == "LeakyRelu" %}
21
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
22
+ {% elif mode == "Sigmoid" %}
23
  return 1.0 / (1.0 + exp(-v));
24
+ {% elif mode == "Tanh" %}
25
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
26
+ // the builtin preserves that saturated result for larger accumulators.
 
27
  return tanh(clamp(v, -10.0, 10.0));
28
+ {% elif mode == "HardSigmoid" %}
29
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
30
  {% else %}
31
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
32
  {% endif %}
33
+ {%- endmacro -%}
34
+ fn fused_act(v: f32) -> f32 {
35
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
36
  }
37
  {% endif %}
38
 
39
+ {% set hasZ = hasZ is defined and hasZ %}
40
  const WG: u32 = {{ convWorkgroupSize }}u;
41
 
42
  @compute @workgroup_size(WG)
43
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
44
+ let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
 
45
  if (index >= params.count) {
46
  return;
47
  }
 
57
  let group = oc / params.outChannelsPerGroup;
58
 
59
  var acc = 0.0;
60
+ {% if hasBias %}
61
  acc = f32(bias[oc]);
62
  {% endif %}
63
  for (var ic = 0u; ic < params.weightInChannels; ic += 1u) {
build/webgpu/conv-direct-unrolled.wgsl.jinja CHANGED
@@ -1,53 +1,55 @@
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
29
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
30
- // already +/-1 to full f32 precision by |v| ~ 9.
31
  return tanh(clamp(v, -10.0, 10.0));
32
- {% elif activation == "HardSigmoid" %}
33
- return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
34
  {% else %}
35
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
36
  {% endif %}
 
 
 
37
  }
38
  {% endif %}
39
 
40
- const KERNEL_AREA: u32 = {{ source.kernelH * source.kernelW }}u;
41
- const STRIDE_H: u32 = {{ source.strideH }}u;
42
- const STRIDE_W: u32 = {{ source.strideW }}u;
43
- const PAD_TOP: i32 = {{ source.padTop }};
44
- const PAD_LEFT: i32 = {{ source.padLeft }};
 
45
 
46
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
47
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
48
  // 2D-folded flat index: gid.y carries the high bits past the
49
- // maxComputeWorkgroupsPerDimension dispatch limit (outputs > 16.7M elements).
50
- let index = gid.x + gid.y * nwg.x * {{ tunables.WORKGROUP_SIZE }}u;
51
  if (index >= params.count) {
52
  return;
53
  }
@@ -71,16 +73,16 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
71
  acc = f32(bias[oc]);
72
  {% endif %}
73
  for (var ic = 0u; ic < params.weightInChannels; ic = ic + 1u) {
74
- {% for kh in range(source.kernelH) %}
75
  {
76
- let ih = ihBase + {{ kh * source.dilationH }}i;
77
  if (ih >= 0 && ih < i32(params.inH)) {
78
  let xRow = xBase + u32(ih) * params.inW;
79
- {% for kw in range(source.kernelW) %}
80
  {
81
- let iw = iwBase + {{ kw * source.dilationW }}i;
82
  if (iw >= 0 && iw < i32(params.inW)) {
83
- acc = acc + f32(x[xRow + u32(iw)]) * f32(w[wBase + {{ kh * source.kernelW + kw }}u]);
84
  }
85
  }
86
  {% endfor %}
 
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 owns each output element and accumulates with input channels outer,
4
+ // then kernel rows and columns in ascending order.
 
5
  // The kh/kw window offsets, kh*KW+kw weight offsets, and the per-channel
6
  // strides fold to literals; the per-row bounds check is hoisted per kh.
7
+ // Group offsets are applied to both input channels and weights. f16 inputs are
8
+ // widened to an f32 accumulator and narrowed once at store.
 
 
 
9
  {{ env.wgsl.resourceDeclarations }}
10
  {% set hasActivation = hasActivation is defined and hasActivation %}
11
+ {% set activation = activation | default("") %}
12
+ {% set actAlpha = actAlpha | default(0.0) %}
13
+ {% set actBeta = actBeta | default(0.0) %}
14
  {% if hasActivation %}
15
  // Apply the fused activation in the f32 accumulator before the single output
16
  // cast, avoiding an intermediate convolution tensor.
17
+ {% macro fused_act_return(mode, alpha, beta) -%}
18
+ {% if mode == "Relu" %}
19
  return max(v, 0.0);
20
+ {% elif mode == "Clip" %}
21
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
22
+ {% elif mode == "LeakyRelu" %}
23
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
24
+ {% elif mode == "Sigmoid" %}
25
  return 1.0 / (1.0 + exp(-v));
26
+ {% elif mode == "Tanh" %}
27
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
28
+ // the builtin preserves that saturated result for larger accumulators.
 
29
  return tanh(clamp(v, -10.0, 10.0));
30
+ {% elif mode == "HardSigmoid" %}
31
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
32
  {% else %}
33
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
34
  {% endif %}
35
+ {%- endmacro -%}
36
+ fn fused_act(v: f32) -> f32 {
37
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
38
  }
39
  {% endif %}
40
 
41
+ {% set hasZ = hasZ is defined and hasZ %}
42
+ const KERNEL_AREA: u32 = {{ kernelHSpec * kernelWSpec }}u;
43
+ const STRIDE_H: u32 = {{ strideHSpec }}u;
44
+ const STRIDE_W: u32 = {{ strideWSpec }}u;
45
+ const PAD_TOP: i32 = {{ padTopSpec }};
46
+ const PAD_LEFT: i32 = {{ padLeftSpec }};
47
 
48
  @compute @workgroup_size({{ tunables.WORKGROUP_SIZE }})
49
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
50
  // 2D-folded flat index: gid.y carries the high bits past the
51
+ // per-axis dispatch fold width (outputs > 16.7M elements).
52
+ let index = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * {{ tunables.WORKGROUP_SIZE }}u;
53
  if (index >= params.count) {
54
  return;
55
  }
 
73
  acc = f32(bias[oc]);
74
  {% endif %}
75
  for (var ic = 0u; ic < params.weightInChannels; ic = ic + 1u) {
76
+ {% for kh in range(kernelHSpec) %}
77
  {
78
+ let ih = ihBase + {{ kh * dilationHSpec }}i;
79
  if (ih >= 0 && ih < i32(params.inH)) {
80
  let xRow = xBase + u32(ih) * params.inW;
81
+ {% for kw in range(kernelWSpec) %}
82
  {
83
+ let iw = iwBase + {{ kw * dilationWSpec }}i;
84
  if (iw >= 0 && iw < i32(params.inW)) {
85
+ acc = acc + f32(x[xRow + u32(iw)]) * f32(w[wBase + {{ kh * kernelWSpec + kw }}u]);
86
  }
87
  }
88
  {% endfor %}
build/webgpu/conv-im2col-nchw.wgsl.jinja CHANGED
@@ -6,20 +6,19 @@
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
@@ -64,10 +63,10 @@ fn main(
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
  }
 
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
+ // A plan for f16 inputs may retain f16 columns to avoid that widening.
10
+ {% set colsScalar = colsScalar | default("f32") %}
 
11
  {{ env.wgsl.resourceDeclarations }}
12
 
13
+ const KERNEL_H: u32 = {{ kernelHSpec }}u;
14
+ const KERNEL_W: u32 = {{ kernelWSpec }}u;
15
  const KSIZE: u32 = KERNEL_H * KERNEL_W;
16
+ const STRIDE_H: u32 = {{ strideHSpec }}u;
17
+ const STRIDE_W: u32 = {{ strideWSpec }}u;
18
+ const DILATION_H: u32 = {{ dilationHSpec }}u;
19
+ const DILATION_W: u32 = {{ dilationWSpec }}u;
20
+ const PAD_TOP: i32 = {{ padTopSpec }};
21
+ const PAD_LEFT: i32 = {{ padLeftSpec }};
22
  {% if padded %}
23
  // Padded materialization for the subgroup-matrix GEMM: the cols buffer is
24
  // K -> ceil(K/32)*32 rows by N -> ceil(N/64)*64 columns, with rows >= kRows and
 
63
  }
64
  {% if padded %}
65
  }
 
 
 
66
  {% endif %}
67
+ {% set columnRows = "K_PADDED" if padded else "params.kRows" %}
68
+ {% set columnStride = "COLS_STRIDE" if padded else "params.outCount" %}
69
+ cols[(batch * {{ columnRows }} + k) * {{ columnStride }} + pos] = {% if colsScalar == "f16" %}f16(value){% else %}value{% endif %};
70
  pos = pos + step;
71
  }
72
  }
build/webgpu/conv-splitk-reduce.wgsl.jinja ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Pass 2 of the split-K Conv GEMM: sum the SPLIT_K raw partial slices that
2
+ // a Conv GEMM partial pass wrote, then apply the bias and epilogue
3
+ // that pass deliberately skipped, and publish the logical [batch, M, N] output.
4
+ //
5
+ // The partial scratch is [SPLIT_K, batch, PARTIAL_ROWS, PARTIAL_COLS] with both
6
+ // trailing axes padded to whole GEMM tiles; this pass indexes only the logical
7
+ // (batch, m, n), so the padded rows and columns are read by nobody and their
8
+ // contents never matter. One output element per thread, walking n fastest so
9
+ // consecutive threads touch consecutive addresses in every slice.
10
+ //
11
+ // Summing slices in index order changes the f32 association relative to one
12
+ // uninterrupted K loop, so the two orders need not be bit-identical.
13
+ {{ env.wgsl.resourceDeclarations }}
14
+ {% set applyActivation = hasActivation is defined and hasActivation %}
15
+ {% if applyActivation %}
16
+ {% set hasActivation = hasActivation is defined and hasActivation %}
17
+ {% set activation = activation | default("") %}
18
+ {% set actAlpha = actAlpha | default(0.0) %}
19
+ {% set actBeta = actBeta | default(0.0) %}
20
+ {% if hasActivation %}
21
+ // Apply the fused activation in the f32 accumulator before the single output
22
+ // cast, avoiding an intermediate convolution tensor.
23
+ {% macro fused_act_return(mode, alpha, beta) -%}
24
+ {% if mode == "Relu" %}
25
+ return max(v, 0.0);
26
+ {% elif mode == "Clip" %}
27
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
28
+ {% elif mode == "LeakyRelu" %}
29
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
30
+ {% elif mode == "Sigmoid" %}
31
+ return 1.0 / (1.0 + exp(-v));
32
+ {% elif mode == "Tanh" %}
33
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
34
+ // the builtin preserves that saturated result for larger accumulators.
35
+ return tanh(clamp(v, -10.0, 10.0));
36
+ {% elif mode == "HardSigmoid" %}
37
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
38
+ {% else %}
39
+ return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
40
+ {% endif %}
41
+ {%- endmacro -%}
42
+ fn fused_act(v: f32) -> f32 {
43
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
44
+ }
45
+ {% endif %}
46
+
47
+ {% endif %}
48
+
49
+ const M: u32 = {{ M }}u;
50
+ const N: u32 = {{ N }}u;
51
+ const SPLIT_K: u32 = {{ splitK }}u;
52
+ const PARTIAL_ROWS: u32 = {{ mPadded }}u;
53
+ const PARTIAL_COLS: u32 = {{ nPadded }}u;
54
+ const PARTIAL_SLICE_STRIDE: u32 = {{ batchCount }}u * PARTIAL_ROWS * PARTIAL_COLS;
55
+ const COUNT: u32 = {{ batchCount }}u * M * N;
56
+ const WORKGROUP_SIZE: u32 = {{ reduceWorkgroupSize }}u;
57
+
58
+ @compute @workgroup_size({{ reduceWorkgroupSize }}, 1, 1)
59
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
60
+ // 2D-folded flat index: gid.y carries the high bits past
61
+ // the per-axis dispatch fold width and reduces to the 1D form at y=0.
62
+ let idx = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WORKGROUP_SIZE;
63
+ if (idx >= COUNT) {
64
+ return;
65
+ }
66
+ let col = idx % N;
67
+ let row = (idx / N) % M;
68
+ let batch = idx / (M * N);
69
+
70
+ var src = (batch * PARTIAL_ROWS + row) * PARTIAL_COLS + col;
71
+ var acc = 0.0;
72
+ for (var z = 0u; z < SPLIT_K; z = z + 1u) {
73
+ acc = acc + partial[src];
74
+ src = src + PARTIAL_SLICE_STRIDE;
75
+ }
76
+ {% if hasBias %}
77
+ acc = acc + f32(bias[row]);
78
+ {% endif %}
79
+ {% if applyActivation %}
80
+ acc = fused_act(acc);
81
+ {% endif %}
82
+ y[idx] = {{ T }}(acc);
83
+ }
build/webgpu/conv1d-tiled-reg.wgsl.jinja CHANGED
@@ -1,48 +1,43 @@
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
38
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
39
- // already +/-1 to full f32 precision by |v| ~ 9.
40
  return tanh(clamp(v, -10.0, 10.0));
41
- {% elif activation == "HardSigmoid" %}
42
- return clamp(f32({{ actAlpha }}) * v + f32({{ actBeta }}), 0.0, 1.0);
43
  {% else %}
44
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
45
  {% endif %}
 
 
 
46
  }
47
  {% endif %}
48
 
 
1
+ // Register-tiled implicit GEMM for group-1 NCW convolution:
2
+ // Y[b] (M x N) = W (M x K) @ im2col(X[b]) (K x N), where M is outChannels, K
3
+ // is inChannels * kernelW, and N is outW. The X tile is gathered directly from
4
+ // NCW with the configured stride, dilation, and left padding; no column matrix
5
+ // is materialized.
6
+ //
7
+ // A WG_X x WG_Y workgroup computes a (WG_Y * TM) x (WG_X * TN) output tile
8
+ // from BK-deep slices, with one TM x TN register micro-tile per thread. Weight
9
+ // and input tiles group four K values per workgroup-memory word and accumulate
10
+ // through dot(). Bounds checks cover every partial tile. Accumulation, bias,
11
+ // and the optional activation epilogue use f32; f16 operands widen at the FMA.
 
 
 
 
 
 
 
 
 
 
12
  {{ env.wgsl.resourceDeclarations }}
13
  {% set hasActivation = hasActivation is defined and hasActivation %}
14
+ {% set activation = activation | default("") %}
15
+ {% set actAlpha = actAlpha | default(0.0) %}
16
+ {% set actBeta = actBeta | default(0.0) %}
17
  {% if hasActivation %}
18
  // Apply the fused activation in the f32 accumulator before the single output
19
  // cast, avoiding an intermediate convolution tensor.
20
+ {% macro fused_act_return(mode, alpha, beta) -%}
21
+ {% if mode == "Relu" %}
22
  return max(v, 0.0);
23
+ {% elif mode == "Clip" %}
24
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
25
+ {% elif mode == "LeakyRelu" %}
26
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
27
+ {% elif mode == "Sigmoid" %}
28
  return 1.0 / (1.0 + exp(-v));
29
+ {% elif mode == "Tanh" %}
30
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
31
+ // the builtin preserves that saturated result for larger accumulators.
 
32
  return tanh(clamp(v, -10.0, 10.0));
33
+ {% elif mode == "HardSigmoid" %}
34
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
35
  {% else %}
36
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
37
  {% endif %}
38
+ {%- endmacro -%}
39
+ fn fused_act(v: f32) -> f32 {
40
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
41
  }
42
  {% endif %}
43
 
build/webgpu/conv2d-grouped-large-w4.wgsl.jinja CHANGED
@@ -1,30 +1,32 @@
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
- // Clamped for the reason given in the GEMM activation: an unbounded accumulator
20
- // overflows the hardware tanh's internal e^2v to Inf/Inf = NaN, and tanh is
21
- // already +/-1 to full f32 precision by |v| ~ 9.
22
  return tanh(clamp(v, -10.0, 10.0));
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
 
@@ -32,86 +34,125 @@ fn fused_act(v: f32) -> f32 {
32
  // value for neighboring output columns and input window for neighboring output
33
  // channels. One invocation computes four adjacent columns for OC_TILE channels,
34
  // sharing each input load across those channels. Shape and window geometry are
35
- // static, so all indexing divisors and kernel offsets are strength-reduced and
36
- // unrolled by the shader compiler.
37
- const COUNT_TILES: u32 = {{ source.countTiles }}u;
38
- {% if source.tailOutput %}const OUT_W: u32 = max(1u, {{ source.outW }}u);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  {% endif %}
40
- const OUT_W4: u32 = max(1u, {{ source.outW4 }}u);
41
- const OUT_H: u32 = max(1u, {{ source.outH }}u);
42
- const OUT_C: u32 = max(1u, {{ source.outC }}u);
43
- const OC_TILE: u32 = {{ source.ocTile }}u;
44
  const OUT_C_TILES: u32 = OUT_C / OC_TILE;
45
- const OUT_CPG: u32 = {{ source.outCPerGroup }}u;
46
- const IN_C: u32 = {{ source.inC }}u;
47
- const IN_CPG: u32 = {{ source.inCPerGroup }}u;
48
- const IN_H: i32 = {{ source.inH }};
49
- const IN_W: i32 = {{ source.inW }};
50
- const IN_H_U: u32 = {{ source.inH }}u;
51
- const IN_W_U: u32 = {{ source.inW }}u;
52
  const IN_PLANE: u32 = IN_H_U * IN_W_U;
53
- {% if not source.tailOutput %}const OUT_PLANE4: u32 = OUT_H * OUT_W4;
54
  {% endif %}
55
- const KAREA: u32 = {{ source.kernelH * source.kernelW }}u;
56
- const STRIDE_H: u32 = {{ source.strideH }}u;
57
- const STRIDE_W: i32 = {{ source.strideW }};
58
- const PAD_TOP: i32 = {{ source.padTop }};
59
- const PAD_LEFT: i32 = {{ source.padLeft }};
60
- const WG: u32 = {{ source.workgroupSize }}u;
61
 
62
- @compute @workgroup_size({{ source.workgroupSize }})
63
- fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups) nwg: vec3<u32>) {
64
- let q = gid.x + gid.y * nwg.x * WG;
65
  if (q >= COUNT_TILES) { return; }
66
 
 
 
 
 
67
  let ow4 = q % OUT_W4;
68
  var t = q / OUT_W4;
 
69
  let oh = t % OUT_H;
70
  t /= OUT_H;
71
  let ocBase = (t % OUT_C_TILES) * OC_TILE;
72
  let batch = t / OUT_C_TILES;
73
  let group = ocBase / OUT_CPG;
74
  let ihBase = i32(oh * STRIDE_H) - PAD_TOP;
 
 
 
 
 
 
75
  let iwBase = i32(ow4 * 4u) * STRIDE_W - PAD_LEFT;
 
76
  var xChannelBase = (batch * IN_C + group * IN_CPG) * IN_PLANE;
77
- {% for oct in range(source.ocTile) %}
78
  var wBase{{ oct }} = (ocBase + {{ oct }}u) * IN_CPG * KAREA;
 
 
 
 
 
79
  var acc{{ oct }} = vec4<f32>(0.0);
 
80
  {% endfor %}
81
 
82
  for (var ic = 0u; ic < IN_CPG; ic++) {
83
- {% for kh in range(source.kernelH) %}
84
  {
85
- let ih = ihBase + {{ kh * source.dilationH }};
86
  if (ih >= 0 && ih < IN_H) {
87
  let xRow = xChannelBase + u32(ih) * IN_W_U;
88
- {% if source.span <= source.spanCap %}
89
  // The four output columns and the kernel taps overlap heavily: tap kw of
90
  // lane l reads input column kw*dilationW + l*strideW, so kernelW*4 reads
91
  // land on only SPAN distinct columns. Load the row window once and let
92
- // every tap take its value from a register. Out-of-range columns hold
93
- // zero, which contributes nothing to the accumulation exactly as the
94
- // per-tap bounds test did.
95
  //
96
- // SPAN grows as (kernelW-1)*dilationW, so a widely dilated kernel would
97
- // want more registers than a thread has and spill. Past spanCap the
98
- // per-tap form below stays, where the register cost is fixed.
99
- {% for s in range(source.span) %}
100
  {% set used = namespace(value=false) %}
101
- {% for kw in range(source.kernelW) %}
102
- {% for lane in range(4) %}
103
- {% if s == kw * source.dilationW + lane * source.strideW %}{% set used.value = true %}{% endif %}
104
  {% endfor %}
105
  {% endfor %}
106
  {% if used.value %}
107
  var xw{{ s }} = 0.0;
108
  {% endif %}
109
  {% endfor %}
110
- {% for s in range(source.span) %}
111
  {% set used = namespace(value=false) %}
112
- {% for kw in range(source.kernelW) %}
113
- {% for lane in range(4) %}
114
- {% if s == kw * source.dilationW + lane * source.strideW %}{% set used.value = true %}{% endif %}
115
  {% endfor %}
116
  {% endfor %}
117
  {% if used.value %}
@@ -121,31 +162,31 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
121
  }
122
  {% endif %}
123
  {% endfor %}
124
- {% for kw in range(source.kernelW) %}
125
  {
126
- {% for oct in range(source.ocTile) %}
127
- let weight{{ oct }} = f32(w[wBase{{ oct }} + {{ kh * source.kernelW + kw }}u]);
128
  {% endfor %}
129
- {% for lane in range(4) %}
130
- {% for oct in range(source.ocTile) %}
131
- acc{{ oct }}.{{ ["x", "y", "z", "w"][lane] }} += xw{{ kw * source.dilationW + lane * source.strideW }} * weight{{ oct }};
132
  {% endfor %}
133
  {% endfor %}
134
  }
135
  {% endfor %}
136
  {% else %}
137
- {% for kw in range(source.kernelW) %}
138
  {
139
- {% for oct in range(source.ocTile) %}
140
- let weight{{ oct }} = f32(w[wBase{{ oct }} + {{ kh * source.kernelW + kw }}u]);
141
  {% endfor %}
142
- let iwK = iwBase + {{ kw * source.dilationW }};
143
- {% for lane in range(4) %}
144
- let iwLane{{ lane }} = iwK + {{ lane * source.strideW }};
145
  if (iwLane{{ lane }} >= 0 && iwLane{{ lane }} < IN_W) {
146
  let xValue = f32(x[xRow + u32(iwLane{{ lane }})]);
147
- {% for oct in range(source.ocTile) %}
148
- acc{{ oct }}.{{ ["x", "y", "z", "w"][lane] }} += xValue * weight{{ oct }};
149
  {% endfor %}
150
  }
151
  {% endfor %}
@@ -156,13 +197,23 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
156
  }
157
  {% endfor %}
158
  xChannelBase += IN_PLANE;
159
- {% for oct in range(source.ocTile) %}
160
  wBase{{ oct }} += KAREA;
161
  {% endfor %}
162
  }
163
- {% if source.tailOutput %}
 
 
 
 
 
 
 
 
 
 
164
  let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W + ow4 * 4u;
165
- {% for oct in range(source.ocTile) %}
166
  let outChannelBase{{ oct }} = outBase + {{ oct }}u * OUT_H * OUT_W;
167
  {% for lane in range(4) %}
168
  if (ow4 * 4u + {{ lane }}u < OUT_W) {
@@ -172,7 +223,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>, @builtin(num_workgroups)
172
  {% endfor %}
173
  {% else %}
174
  let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W4 + ow4;
175
- {% for oct in range(source.ocTile) %}
176
  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 %});
177
  {% endfor %}
178
  {% endif %}
 
 
 
 
1
  {{ env.wgsl.resourceDeclarations }}
2
  {% set hasActivation = hasActivation is defined and hasActivation %}
3
+ {% set activation = activation | default("") %}
4
+ {% set actAlpha = actAlpha | default(0.0) %}
5
+ {% set actBeta = actBeta | default(0.0) %}
6
  {% if hasActivation %}
7
  // Apply the fused activation in the f32 accumulator before the single output
8
  // cast, avoiding an intermediate convolution tensor.
9
+ {% macro fused_act_return(mode, alpha, beta) -%}
10
+ {% if mode == "Relu" %}
11
  return max(v, 0.0);
12
+ {% elif mode == "Clip" %}
13
+ return clamp(v, f32({{ alpha }}), f32({{ beta }}));
14
+ {% elif mode == "LeakyRelu" %}
15
+ return select(v * f32({{ alpha }}), v, v >= 0.0);
16
+ {% elif mode == "Sigmoid" %}
17
  return 1.0 / (1.0 + exp(-v));
18
+ {% elif mode == "Tanh" %}
19
+ // tanh is already +/-1 to f32 precision at the clamp bounds. Clamping before
20
+ // the builtin preserves that saturated result for larger accumulators.
 
21
  return tanh(clamp(v, -10.0, 10.0));
22
+ {% elif mode == "HardSigmoid" %}
23
+ return clamp(f32({{ alpha }}) * v + f32({{ beta }}), 0.0, 1.0);
24
  {% else %}
25
  return v * clamp(v * 0.16666666666666666 + 0.5, 0.0, 1.0);
26
  {% endif %}
27
+ {%- endmacro -%}
28
+ fn fused_act(v: f32) -> f32 {
29
+ {{ fused_act_return(activation, actAlpha, actBeta) -}}
30
  }
31
  {% endif %}
32
 
 
34
  // value for neighboring output columns and input window for neighboring output
35
  // channels. One invocation computes four adjacent columns for OC_TILE channels,
36
  // sharing each input load across those channels. Shape and window geometry are
37
+ // static, so indexing divisors and kernel offsets are shader constants.
38
+ {% set dilatedLanes = dilatedLanes | default(false) %}
39
+ {% set lanes = lanes | default(4) %}
40
+ {% set quads = quads | default(1) %}
41
+ {% set laneStep = dilationWSpec if dilatedLanes else strideWSpec %}
42
+ {% set comps = ["x", "y", "z", "w"] %}
43
+ {% macro acc_ref(oct, lane) %}{% if dilatedLanes %}acc{{ oct }}_{{ lane }}{% else %}acc{{ oct }}.{{ comps[lane] }}{% endif %}{% endmacro %}
44
+ {% set usedCount = namespace(value=0) %}
45
+ {% for s in range(span) %}
46
+ {% set used = namespace(value=false) %}
47
+ {% for kw in range(kernelWSpec) %}
48
+ {% for lane in range(lanes) %}
49
+ {% if s == kw * dilationWSpec + lane * laneStep %}{% set used.value = true %}{% endif %}
50
+ {% endfor %}
51
+ {% endfor %}
52
+ {% if used.value %}{% set usedCount.value = usedCount.value + 1 %}{% endif %}
53
+ {% endfor %}
54
+ {% set registerForm = (usedCount.value <= spanCap) if dilatedLanes else (span <= spanCap) %}
55
+ const COUNT_TILES: u32 = {{ countTiles }}u;
56
+ {% if tailOutput %}const OUT_W: u32 = max(1u, {{ outW }}u);
57
+ {% endif %}
58
+ {% if dilatedLanes %}
59
+ const OUT_W: u32 = max(1u, {{ outW }}u);
60
+ const LANES: u32 = {{ lanes }}u;
61
+ const RESIDUES: u32 = {{ dilationWSpec }}u;
62
+ const TILES_PER_ROW: u32 = RESIDUES * {{ quads }}u;
63
+ {% else %}
64
+ const OUT_W4: u32 = max(1u, {{ outW4 }}u);
65
  {% endif %}
66
+ const OUT_H: u32 = max(1u, {{ outH }}u);
67
+ const OUT_C: u32 = max(1u, {{ outC }}u);
68
+ const OC_TILE: u32 = {{ ocTile }}u;
 
69
  const OUT_C_TILES: u32 = OUT_C / OC_TILE;
70
+ const OUT_CPG: u32 = {{ outCPerGroup }}u;
71
+ const IN_C: u32 = {{ inC }}u;
72
+ const IN_CPG: u32 = {{ inCPerGroup }}u;
73
+ const IN_H: i32 = {{ inH }};
74
+ const IN_W: i32 = {{ inW }};
75
+ const IN_H_U: u32 = {{ inH }}u;
76
+ const IN_W_U: u32 = {{ inW }}u;
77
  const IN_PLANE: u32 = IN_H_U * IN_W_U;
78
+ {% if not tailOutput and not dilatedLanes %}const OUT_PLANE4: u32 = OUT_H * OUT_W4;
79
  {% endif %}
80
+ const KAREA: u32 = {{ kernelHSpec * kernelWSpec }}u;
81
+ const STRIDE_H: u32 = {{ strideHSpec }}u;
82
+ const STRIDE_W: i32 = {{ strideWSpec }};
83
+ const PAD_TOP: i32 = {{ padTopSpec }};
84
+ const PAD_LEFT: i32 = {{ padLeftSpec }};
85
+ const WG: u32 = {{ workgroupSizeSpec }}u;
86
 
87
+ @compute @workgroup_size({{ workgroupSizeSpec }})
88
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
89
+ let q = gid.x + gid.y * {{ DISPATCH_FOLD_WIDTH }}u * WG;
90
  if (q >= COUNT_TILES) { return; }
91
 
92
+ {% if dilatedLanes %}
93
+ let tile = q % TILES_PER_ROW;
94
+ var t = q / TILES_PER_ROW;
95
+ {% else %}
96
  let ow4 = q % OUT_W4;
97
  var t = q / OUT_W4;
98
+ {% endif %}
99
  let oh = t % OUT_H;
100
  t /= OUT_H;
101
  let ocBase = (t % OUT_C_TILES) * OC_TILE;
102
  let batch = t / OUT_C_TILES;
103
  let group = ocBase / OUT_CPG;
104
  let ihBase = i32(oh * STRIDE_H) - PAD_TOP;
105
+ {% if dilatedLanes %}
106
+ // Lane l owns output column ow0 + l * dilationW: the residue class first,
107
+ // then the LANES-wide run within it. Runs past the row store nothing.
108
+ let ow0 = tile % RESIDUES + (tile / RESIDUES) * (LANES * RESIDUES);
109
+ let iwBase = i32(ow0) * STRIDE_W - PAD_LEFT;
110
+ {% else %}
111
  let iwBase = i32(ow4 * 4u) * STRIDE_W - PAD_LEFT;
112
+ {% endif %}
113
  var xChannelBase = (batch * IN_C + group * IN_CPG) * IN_PLANE;
114
+ {% for oct in range(ocTile) %}
115
  var wBase{{ oct }} = (ocBase + {{ oct }}u) * IN_CPG * KAREA;
116
+ {% if dilatedLanes %}
117
+ {% for lane in range(lanes) %}
118
+ var acc{{ oct }}_{{ lane }} = 0.0;
119
+ {% endfor %}
120
+ {% else %}
121
  var acc{{ oct }} = vec4<f32>(0.0);
122
+ {% endif %}
123
  {% endfor %}
124
 
125
  for (var ic = 0u; ic < IN_CPG; ic++) {
126
+ {% for kh in range(kernelHSpec) %}
127
  {
128
+ let ih = ihBase + {{ kh * dilationHSpec }};
129
  if (ih >= 0 && ih < IN_H) {
130
  let xRow = xChannelBase + u32(ih) * IN_W_U;
131
+ {% if registerForm %}
132
  // The four output columns and the kernel taps overlap heavily: tap kw of
133
  // lane l reads input column kw*dilationW + l*strideW, so kernelW*4 reads
134
  // land on only SPAN distinct columns. Load the row window once and let
135
+ // every tap take its value from a statically named local. Out-of-range
136
+ // columns contain zero and contribute nothing to the accumulation.
 
137
  //
138
+ // SPAN grows as (kernelW-1)*dilationW. `spanCap` bounds the number of
139
+ // staged row-window values; wider spans use the fixed-size per-tap form below.
140
+ {% for s in range(span) %}
 
141
  {% set used = namespace(value=false) %}
142
+ {% for kw in range(kernelWSpec) %}
143
+ {% for lane in range(lanes) %}
144
+ {% if s == kw * dilationWSpec + lane * laneStep %}{% set used.value = true %}{% endif %}
145
  {% endfor %}
146
  {% endfor %}
147
  {% if used.value %}
148
  var xw{{ s }} = 0.0;
149
  {% endif %}
150
  {% endfor %}
151
+ {% for s in range(span) %}
152
  {% set used = namespace(value=false) %}
153
+ {% for kw in range(kernelWSpec) %}
154
+ {% for lane in range(lanes) %}
155
+ {% if s == kw * dilationWSpec + lane * laneStep %}{% set used.value = true %}{% endif %}
156
  {% endfor %}
157
  {% endfor %}
158
  {% if used.value %}
 
162
  }
163
  {% endif %}
164
  {% endfor %}
165
+ {% for kw in range(kernelWSpec) %}
166
  {
167
+ {% for oct in range(ocTile) %}
168
+ let weight{{ oct }} = f32(w[wBase{{ oct }} + {{ kh * kernelWSpec + kw }}u]);
169
  {% endfor %}
170
+ {% for lane in range(lanes) %}
171
+ {% for oct in range(ocTile) %}
172
+ {{ acc_ref(oct, lane) }} += xw{{ kw * dilationWSpec + lane * laneStep }} * weight{{ oct }};
173
  {% endfor %}
174
  {% endfor %}
175
  }
176
  {% endfor %}
177
  {% else %}
178
+ {% for kw in range(kernelWSpec) %}
179
  {
180
+ {% for oct in range(ocTile) %}
181
+ let weight{{ oct }} = f32(w[wBase{{ oct }} + {{ kh * kernelWSpec + kw }}u]);
182
  {% endfor %}
183
+ let iwK = iwBase + {{ kw * dilationWSpec }};
184
+ {% for lane in range(lanes) %}
185
+ let iwLane{{ lane }} = iwK + {{ lane * laneStep }};
186
  if (iwLane{{ lane }} >= 0 && iwLane{{ lane }} < IN_W) {
187
  let xValue = f32(x[xRow + u32(iwLane{{ lane }})]);
188
+ {% for oct in range(ocTile) %}
189
+ {{ acc_ref(oct, lane) }} += xValue * weight{{ oct }};
190
  {% endfor %}
191
  }
192
  {% endfor %}
 
197
  }
198
  {% endfor %}
199
  xChannelBase += IN_PLANE;
200
+ {% for oct in range(ocTile) %}
201
  wBase{{ oct }} += KAREA;
202
  {% endfor %}
203
  }
204
+ {% if dilatedLanes %}
205
+ let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W;
206
+ {% for oct in range(ocTile) %}
207
+ let outChannelBase{{ oct }} = outBase + {{ oct }}u * OUT_H * OUT_W;
208
+ {% for lane in range(lanes) %}
209
+ if (ow0 + {{ lane * dilationWSpec }}u < OUT_W) {
210
+ y[outChannelBase{{ oct }} + ow0 + {{ lane * dilationWSpec }}u] = {{ scalar }}({% if hasActivation %}fused_act(acc{{ oct }}_{{ lane }}){% else %}acc{{ oct }}_{{ lane }}{% endif %});
211
+ }
212
+ {% endfor %}
213
+ {% endfor %}
214
+ {% elif tailOutput %}
215
  let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W + ow4 * 4u;
216
+ {% for oct in range(ocTile) %}
217
  let outChannelBase{{ oct }} = outBase + {{ oct }}u * OUT_H * OUT_W;
218
  {% for lane in range(4) %}
219
  if (ow4 * 4u + {{ lane }}u < OUT_W) {
 
223
  {% endfor %}
224
  {% else %}
225
  let outBase = ((batch * OUT_C + ocBase) * OUT_H + oh) * OUT_W4 + ow4;
226
+ {% for oct in range(ocTile) %}
227
  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 %});
228
  {% endfor %}
229
  {% endif %}
build/webgpu/manifest.json CHANGED
The diff for this file is too large to render. See raw diff
 
build/webgpu/metadata.json CHANGED
@@ -1,25 +1,91 @@
1
  {
2
  "name": "com.microsoft.FusedConv",
3
- "id": "_com_microsoft_fusedconv_webgpu_e37405a",
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": "n0CRMXMLwCsmBCTq9ruaSshgRFN+QcWfYHmLLT/nYDg=",
12
- "conv-1x1-gemm-tiled.wgsl.jinja": "tAifcSzQFhljZg7xs7W7xJ1t92ZjRlCCfET/7sxsrzI=",
13
- "conv-1x1-subgroup-matrix.wgsl.jinja": "4M+d7l/v8cK31b+6c2QaD20PSrT3EcpMNT5LV14H+wg=",
14
- "conv-direct-nd.wgsl.jinja": "+rZdtrCmC5XWDKzniyQgM8ZuEPelUn15Wy8XASMqRoI=",
15
- "conv-direct-unrolled.wgsl.jinja": "Z8nzgvMffIyDdiORizHznKPUpi70ZcWPZvmwhnuE+PQ=",
16
- "conv-im2col-nchw.wgsl.jinja": "7IUuXo33elZx0pnR9M2vUXzXDjFuw2bJODkZViV/WQg=",
17
- "conv1d-tiled-reg.wgsl.jinja": "f6XtRoLCTQvmBsoTWjymS9jK1ewlj9R2t+DWBa7vv/0=",
18
- "conv2d-grouped-large-w4.wgsl.jinja": "HMab+h8mlTI14B1EdMAkYmwFRFkRipfdT2ZOx36X2dY=",
19
- "manifest.json": "jMjmEiSVgMOjNheSacCaHlppVU7uWplUp2lv3fvGH1A=",
20
- "test.json": "/KodhC9ZJ6IxO1wokLmeGH0zsOfDpds9+mjZLZRnPXw="
 
21
  }
22
  },
23
- "provenance": { "kernel": { "sha": "c928d21e6cc1310861cba3bafb75f5f679ecf5f3", "dirty": false } },
24
- "webgpu": { "manifestSpec": "1.0", "specialized": true, "opPath": "ops/com.microsoft.FusedConv" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
1
  {
2
  "name": "com.microsoft.FusedConv",
3
+ "id": "_com_microsoft_fusedconv_webgpu_e4dcf04",
4
  "version": 1,
5
  "license": "Apache-2.0",
6
  "backend": { "type": "webgpu" },
7
  "digest": {
8
  "algorithm": "sha256",
9
  "files": {
10
+ "bench.json": "rDPb1+xln8/qfupw2ajiwaBUEKJvlo+NuJEs81cAgTk=",
11
+ "conv-1x1-gemm-tiled-reg.wgsl.jinja": "9yFpwXS1HzmWmm4W72n+aOIryLqc9+e88awZ1ogTe+Q=",
12
+ "conv-1x1-gemm-tiled.wgsl.jinja": "uqG5mRVvcHiUo0LDCUcVgqElVTvRNknx19Gm4oIALiU=",
13
+ "conv-1x1-subgroup-matrix.wgsl.jinja": "iDJs4RJk4iJarOZCjY7/5MFrHgR7CcqI6x5uwv2dYaA=",
14
+ "conv-direct-nd.wgsl.jinja": "5UA6BigLbtPRMre3LRfNShldvupB5yTGn8EzX2tBzP8=",
15
+ "conv-direct-unrolled.wgsl.jinja": "vrGUPwYkbYE1slII7nuK5wbERRcG5u0X1cW1kMc2sQs=",
16
+ "conv-im2col-nchw.wgsl.jinja": "IRpmtpeVQsLE6ud+Sf1230XaZVS1dxDtTZuSDwU3P8s=",
17
+ "conv-splitk-reduce.wgsl.jinja": "LbVU5DRL0Q51fvLjThmxJQZH8aPbZkvNzij4nY/cHmo=",
18
+ "conv1d-tiled-reg.wgsl.jinja": "ERqOPl+1RbnJ+CFqZvLSYw45qYMKgB7Je0teew5gyOM=",
19
+ "conv2d-grouped-large-w4.wgsl.jinja": "qBG2cfOsQ4FMwtNT8wBOqLJFDOs6w0wJCmfcZuxbho8=",
20
+ "manifest.json": "EpZ3lQZ0Ar7ZY4xDkUlcQ3m6lyw6x6+cp9KoBC47TSE=",
21
+ "test.json": "g7doPXd+oCU97EVAbYReA8qInOswdv5wrLcMYCX7CcM="
22
  }
23
  },
24
+ "provenance": { "kernel": { "sha": "91d990483a174128daf7673f3f37a7c890493ae1", "dirty": false } },
25
+ "webgpu": {
26
+ "manifestSpec": "2.0",
27
+ "variants": {
28
+ "implicit_im2col_tiled_reg_splitk": ["conv-1x1-gemm-tiled-reg.wgsl.jinja", "conv-splitk-reduce.wgsl.jinja"],
29
+ "implicit_im2col_tiled_bias_reg_splitk": ["conv-1x1-gemm-tiled-reg.wgsl.jinja", "conv-splitk-reduce.wgsl.jinja"],
30
+ "implicit_im2col_tiled_reg_splitk_preferred": ["conv-1x1-gemm-tiled-reg.wgsl.jinja", "conv-splitk-reduce.wgsl.jinja"],
31
+ "implicit_im2col_tiled_bias_reg_splitk_preferred": ["conv-1x1-gemm-tiled-reg.wgsl.jinja", "conv-splitk-reduce.wgsl.jinja"],
32
+ "implicit_im2col_tiled_reg": ["conv-1x1-gemm-tiled-reg.wgsl.jinja"],
33
+ "implicit_im2col_tiled_bias_reg": ["conv-1x1-gemm-tiled-reg.wgsl.jinja"],
34
+ "gemm_1x1_subgroup_matrix": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
35
+ "gemm_1x1_subgroup_matrix_z": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
36
+ "gemm_1x1_subgroup_matrix_bias": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
37
+ "gemm_1x1_subgroup_matrix_bias_z": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
38
+ "im2col_gemm_subgroup_matrix": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
39
+ "im2col_direct_inputs_subgroup_matrix": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
40
+ "im2col_gemm_subgroup_matrix_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
41
+ "im2col_direct_inputs_subgroup_matrix_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
42
+ "im2col_gemm_subgroup_matrix_bias": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
43
+ "im2col_direct_inputs_subgroup_matrix_bias": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
44
+ "im2col_gemm_subgroup_matrix_bias_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
45
+ "im2col_direct_inputs_subgroup_matrix_bias_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
46
+ "im2col_half_direct_subgroup_matrix": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
47
+ "im2col_half_direct_subgroup_matrix_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
48
+ "im2col_half_direct_subgroup_matrix_bias": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
49
+ "im2col_half_direct_subgroup_matrix_bias_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
50
+ "im2col_gemm_subgroup_matrix_padded": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
51
+ "im2col_gemm_subgroup_matrix_padded_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
52
+ "im2col_gemm_subgroup_matrix_padded_bias": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
53
+ "im2col_gemm_subgroup_matrix_padded_bias_z": ["conv-1x1-subgroup-matrix.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
54
+ "im2col_gemm_tiled": ["conv-1x1-gemm-tiled.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
55
+ "im2col_gemm_tiled_z": ["conv-1x1-gemm-tiled.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
56
+ "im2col_gemm_tiled_bias": ["conv-1x1-gemm-tiled.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
57
+ "im2col_gemm_tiled_bias_z": ["conv-1x1-gemm-tiled.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
58
+ "im2col_gemm_tiled_reg": ["conv-1x1-gemm-tiled-reg.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
59
+ "im2col_gemm_tiled_bias_reg": ["conv-1x1-gemm-tiled-reg.wgsl.jinja", "conv-im2col-nchw.wgsl.jinja"],
60
+ "gemm_1x1_tiled": ["conv-1x1-gemm-tiled.wgsl.jinja"],
61
+ "gemm_1x1_tiled_z": ["conv-1x1-gemm-tiled.wgsl.jinja"],
62
+ "gemm_1x1_tiled_bias": ["conv-1x1-gemm-tiled.wgsl.jinja"],
63
+ "gemm_1x1_tiled_bias_z": ["conv-1x1-gemm-tiled.wgsl.jinja"],
64
+ "gemm_1x1_tiled_reg": ["conv-1x1-gemm-tiled-reg.wgsl.jinja"],
65
+ "gemm_1x1_tiled_bias_reg": ["conv-1x1-gemm-tiled-reg.wgsl.jinja"],
66
+ "conv1d_direct": ["conv-direct-unrolled.wgsl.jinja"],
67
+ "direct_unrolled": ["conv-direct-unrolled.wgsl.jinja"],
68
+ "direct_unrolled_z": ["conv-direct-unrolled.wgsl.jinja"],
69
+ "conv1d_direct_bias": ["conv-direct-unrolled.wgsl.jinja"],
70
+ "direct_unrolled_bias": ["conv-direct-unrolled.wgsl.jinja"],
71
+ "direct_unrolled_bias_z": ["conv-direct-unrolled.wgsl.jinja"],
72
+ "conv1d_tiled_reg": ["conv1d-tiled-reg.wgsl.jinja"],
73
+ "conv1d_tiled_bias_reg": ["conv1d-tiled-reg.wgsl.jinja"],
74
+ "ncdhw3d": ["conv-direct-nd.wgsl.jinja"],
75
+ "ncdhw3d_z": ["conv-direct-nd.wgsl.jinja"],
76
+ "ncdhw3d_bias": ["conv-direct-nd.wgsl.jinja"],
77
+ "ncdhw3d_bias_z": ["conv-direct-nd.wgsl.jinja"],
78
+ "grouped_large_kernel_w4": ["conv2d-grouped-large-w4.wgsl.jinja"],
79
+ "grouped_large_kernel_w4_tail": ["conv2d-grouped-large-w4.wgsl.jinja"],
80
+ "grouped_large_kernel_w4_dilated_lanes": ["conv2d-grouped-large-w4.wgsl.jinja"],
81
+ "implicit_im2col_subgroup_matrix": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
82
+ "implicit_im2col_subgroup_matrix_z": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
83
+ "implicit_im2col_subgroup_matrix_bias": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
84
+ "implicit_im2col_subgroup_matrix_bias_z": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
85
+ "implicit_im2col_subgroup_matrix_f16": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
86
+ "implicit_im2col_subgroup_matrix_z_f16": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
87
+ "implicit_im2col_subgroup_matrix_bias_f16": ["conv-1x1-subgroup-matrix.wgsl.jinja"],
88
+ "implicit_im2col_subgroup_matrix_bias_z_f16": ["conv-1x1-subgroup-matrix.wgsl.jinja"]
89
+ }
90
+ }
91
  }
build/webgpu/test.json CHANGED
The diff for this file is too large to render. See raw diff