Skip to content

Commit fc62ba7

Browse files
authored
[ExecuTorch][WebGPU] Run stride == kernel conv_transpose2d as a GEMM (#23880)
Summary: conv_transpose2d uses a per-output gather kernel. When stride == kernel (e.g. 2x upsampling) each output takes exactly one tap, so the op is a GEMM plus a depth-to-space scatter. Add conv_transpose2d_gemm.wgsl for that case (groups == 1, no padding, dilation or output_padding). Other shapes, GEMMs too narrow or small to beat the gather, and outputs whose GEMM grid would exceed the dispatch limit keep the gather. Updates the generated-output manifest snapshot for the new shader. This change was authored with Claude Code. Test Plan: - Op tests transpose2x_tiled (multi-tile), transpose3x (GEMM), transpose2x, transpose_overlap (gather) and transpose2x_grid_limit (heavy, gather fallback); all conv2d cases pass, as do the WebGPU Python tests. - Negative controls: breaking the GEMM path fails its cases; dropping the grid check fails transpose2x_grid_limit. - Selection thresholds from GPU timings of GEMM vs gather across channel counts.
1 parent 86c326f commit fc62ba7

6 files changed

Lines changed: 385 additions & 35 deletions

File tree

‎backends/webgpu/runtime/WebGPUShaderRegistry.cpp‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv2d_gemm_wgsl.h>
4949
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv2d_vec4_wgsl.h>
5050
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv2d_wgsl.h>
51+
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv_transpose2d_gemm_wgsl.h>
5152
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv_transpose2d_wgsl.h>
5253
#include <executorch/backends/webgpu/runtime/ops/et_vk_sdpa/et_vk_sdpa_av_wgsl.h>
5354
#include <executorch/backends/webgpu/runtime/ops/et_vk_sdpa/et_vk_sdpa_qk_entry_wgsl.h>
@@ -155,7 +156,7 @@
155156
namespace executorch::backends::webgpu {
156157
namespace {
157158

158-
constexpr std::array<WebGPUShaderInfo, 137> kShaderRegistry = {{
159+
constexpr std::array<WebGPUShaderInfo, 138> kShaderRegistry = {{
159160
{
160161
"abs",
161162
kAbsWGSL,
@@ -429,6 +430,13 @@ constexpr std::array<WebGPUShaderInfo, 137> kShaderRegistry = {{
429430
kConvTranspose2dWorkgroupSizeY,
430431
kConvTranspose2dWorkgroupSizeZ,
431432
},
433+
{
434+
"conv_transpose2d_gemm",
435+
kConvTranspose2dGemmWGSL,
436+
kConvTranspose2dGemmWorkgroupSizeX,
437+
kConvTranspose2dGemmWorkgroupSizeY,
438+
kConvTranspose2dGemmWorkgroupSizeZ,
439+
},
432440
{
433441
"conv_with_clamp",
434442
kConvWithClampWGSL,

‎backends/webgpu/runtime/ops/et_vk_conv2d/Conv2d.cpp‎

Lines changed: 76 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv2d_gemm_wgsl.h>
1414
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv2d_vec4_wgsl.h>
1515
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv2d_wgsl.h>
16+
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv_transpose2d_gemm_wgsl.h>
1617
#include <executorch/backends/webgpu/runtime/ops/et_vk_conv2d/conv_transpose2d_wgsl.h>
1718

1819
#include <webgpu/webgpu.h>
@@ -50,6 +51,13 @@ static_assert(
5051
sizeof(ConvParams) == 80,
5152
"ConvParams must be 80 bytes (16-mult)");
5253

54+
// Below these a 32x32x32 GEMM tile is mostly padding, or the GEMM launches too
55+
// few tiles to fill the GPU, and the direct or gather kernel is as fast or
56+
// faster.
57+
constexpr uint64_t kGemmMinM = 8u;
58+
constexpr uint64_t kGemmMinK = 16u;
59+
constexpr uint64_t kGemmMinWorkgroups = 256u;
60+
5361
// Transposed 2D convolution (gather form), folded into the convolution handler
5462
// (C2: a 2nd WEBGPU_REGISTER_OP(aten.convolution.default) would be silently
5563
// dropped). weight layout = torch convT [IC, OC/groups, KH, KW]. CPU-derisked
@@ -149,15 +157,44 @@ void conv_transpose2d_impl(WebGPUGraph& graph, const std::vector<int>& args) {
149157
const bool has_bias =
150158
graph.get_value_type(bias_id) == WebGPUGraph::ValueType::Tensor;
151159

160+
// Non-overlapping taps (stride == kernel, e.g. 2x upsampling) reduce to a
161+
// GEMM (M = OC*KH*KW, N = input pixels, K = IC) plus a depth-to-space
162+
// scatter. Other shapes, GEMMs too small to pay off, and GEMM tile grids past
163+
// the dispatch limit use the per-output gather (which folds its grid).
164+
constexpr uint32_t kConvTranspose2dGemmTile = 32u; // TILE in the shader
165+
const uint64_t max_wgs = utils::queried_max_workgroups(device);
166+
const uint64_t gemm_m = uint64_t(OC) * KH * KW;
167+
const uint64_t gemm_cols =
168+
utils::div_up<uint64_t>(uint64_t(B) * IH * IW, kConvTranspose2dGemmTile);
169+
const uint64_t gemm_rows =
170+
utils::div_up<uint64_t>(gemm_m, kConvTranspose2dGemmTile);
171+
const bool use_gemm = groups == 1 && sH == KH && sW == KW && pH == 0 &&
172+
pW == 0 && dH == 1 && dW == 1 && opH == 0 && opW == 0 &&
173+
gemm_m >= kGemmMinM && IC >= kGemmMinK &&
174+
gemm_cols * gemm_rows >= kGemmMinWorkgroups && gemm_cols <= max_wgs &&
175+
gemm_rows <= max_wgs;
176+
152177
// Up-front (throw before any buffer alloc -> no leak-on-throw).
153-
// Adaptive 1D->2D dispatch: wg=clamp(device,256) + 2D-spill past the 65535
154-
// ceiling (FpnNeck @1008^2 out_numel exceeds it). stride_x lets the shader
155-
// decode i = gid.y*stride_x + gid.x.
156-
utils::DispatchGrid grid = utils::compute_dispatch_grid(
157-
device,
158-
utils::checked_u32(out_numel, "conv_transpose2d"),
159-
kConvTranspose2dWorkgroupSizeX,
160-
"conv_transpose2d");
178+
utils::WgCount gemm_grid = {};
179+
utils::DispatchGrid grid = {};
180+
if (use_gemm) {
181+
gemm_grid = utils::compute_tile_grid_2d(
182+
device,
183+
utils::checked_u32(
184+
uint64_t(B) * uint64_t(IH) * uint64_t(IW), "conv_transpose2d_gemm"),
185+
OC * KH * KW,
186+
kConvTranspose2dGemmTile,
187+
"conv_transpose2d_gemm");
188+
} else {
189+
// Adaptive 1D->2D dispatch: wg=clamp(device,256) + 2D-spill past the 65535
190+
// ceiling for large outputs. stride_x lets the shader decode
191+
// i = gid.y*stride_x + gid.x.
192+
grid = utils::compute_dispatch_grid(
193+
device,
194+
utils::checked_u32(out_numel, "conv_transpose2d"),
195+
kConvTranspose2dWorkgroupSizeX,
196+
"conv_transpose2d");
197+
}
161198

162199
ConvParams params = {};
163200
params.B = B;
@@ -187,29 +224,38 @@ void conv_transpose2d_impl(WebGPUGraph& graph, const std::vector<int>& args) {
187224
has_bias,
188225
has_bias ? graph.get_tensor(bias_id).buffer : nullptr,
189226
has_bias ? graph.get_tensor(bias_id).nbytes : 0);
190-
auto constants = utils::make_grid_constants(grid);
227+
const std::vector<utils::BindingSpec> bindings = {
228+
{0, WGPUBufferBindingType_Storage, out.buffer, out.nbytes},
229+
{1, WGPUBufferBindingType_ReadOnlyStorage, in.buffer, in.nbytes},
230+
{2, WGPUBufferBindingType_ReadOnlyStorage, weight.buffer, weight.nbytes},
231+
{3, WGPUBufferBindingType_ReadOnlyStorage, bias.buffer, bias.nbytes},
232+
{4, WGPUBufferBindingType_Uniform, uniform_buffer, sizeof(ConvParams)},
233+
};
191234

192-
utils::ComputePipelineBundle bundle = utils::make_compute_pipeline(
193-
device,
194-
kConvTranspose2dWGSL,
195-
{
196-
{0, WGPUBufferBindingType_Storage, out.buffer, out.nbytes},
197-
{1, WGPUBufferBindingType_ReadOnlyStorage, in.buffer, in.nbytes},
198-
{2,
199-
WGPUBufferBindingType_ReadOnlyStorage,
200-
weight.buffer,
201-
weight.nbytes},
202-
{3, WGPUBufferBindingType_ReadOnlyStorage, bias.buffer, bias.nbytes},
203-
{4,
204-
WGPUBufferBindingType_Uniform,
205-
uniform_buffer,
206-
sizeof(ConvParams)},
207-
},
208-
constants.data(),
209-
constants.size());
210-
211-
graph.add_dispatch_2d(
212-
bundle.pipeline, bundle.bind_group, grid.count_x, grid.count_y);
235+
if (use_gemm) {
236+
utils::ComputePipelineBundle bundle = utils::make_compute_pipeline(
237+
device, kConvTranspose2dGemmWGSL, bindings);
238+
graph.add_dispatch(
239+
{bundle.pipeline,
240+
bundle.bind_group,
241+
gemm_grid.x,
242+
"conv_transpose2d_gemm",
243+
gemm_grid.y});
244+
} else {
245+
auto constants = utils::make_grid_constants(grid);
246+
utils::ComputePipelineBundle bundle = utils::make_compute_pipeline(
247+
device,
248+
kConvTranspose2dWGSL,
249+
bindings,
250+
constants.data(),
251+
constants.size());
252+
graph.add_dispatch(
253+
{bundle.pipeline,
254+
bundle.bind_group,
255+
grid.count_x,
256+
"conv_transpose2d",
257+
grid.count_y});
258+
}
213259

214260
wgpuBufferRelease(uniform_buffer);
215261
if (bias.owned_dummy != nullptr) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
@group(0) @binding(0) var<storage, read_write> out: array<f32>;
2+
@group(0) @binding(1) var<storage, read> input: array<f32>;
3+
@group(0) @binding(2) var<storage, read> weight: array<f32>;
4+
@group(0) @binding(3) var<storage, read> bias: array<f32>;
5+
6+
struct Params {
7+
B: u32, IC: u32, IH: u32, IW: u32,
8+
OC: u32, OH: u32, OW: u32, KH: u32, KW: u32,
9+
sH: u32, sW: u32, pH: u32, pW: u32, dH: u32, dW: u32,
10+
groups: u32, has_bias: u32, _p0: u32, _p1: u32, _p2: u32,
11+
}
12+
@group(0) @binding(4) var<uniform> params: Params;
13+
14+
// Non-overlapping transposed conv (stride == kernel, no padding/dilation,
15+
// groups == 1): every output pixel takes exactly one kernel tap, so it is a
16+
// GEMM M=OC*KH*KW, N=B*IH*IW, K=IC followed by a depth-to-space scatter.
17+
// A = weight [IC, OC, KH, KW] read transposed (A[m,k] = weight[k*M + m]); B =
18+
// input [B, IC, IH*IW]. Same tiling as conv2d_gemm (TILE=32, RPT=4).
19+
const TILE: u32 = 32u;
20+
const RPT: u32 = 4u;
21+
22+
var<workgroup> a_sub: array<array<f32, 32>, 32>;
23+
var<workgroup> b_sub: array<array<f32, 32>, 32>;
24+
25+
fn gM() -> u32 { return params.OC * params.KH * params.KW; }
26+
fn gN() -> u32 { return params.B * params.IH * params.IW; }
27+
28+
fn read_a(m: u32, k: u32) -> f32 {
29+
if (m < gM() && k < params.IC) {
30+
return weight[k * gM() + m];
31+
}
32+
return 0.0;
33+
}
34+
35+
fn read_b(k: u32, n: u32) -> f32 {
36+
if (k >= params.IC || n >= gN()) {
37+
return 0.0;
38+
}
39+
let ihw = params.IH * params.IW;
40+
return input[((n / ihw) * params.IC + k) * ihw + n % ihw];
41+
}
42+
43+
@compute @workgroup_size(8, 8, 1)
44+
fn main(
45+
@builtin(workgroup_id) wg_id: vec3<u32>,
46+
@builtin(local_invocation_id) local_id: vec3<u32>) {
47+
let M = gM();
48+
let N = gN();
49+
let K = params.IC;
50+
let tile_row0 = wg_id.y * TILE;
51+
let tile_col0 = wg_id.x * TILE;
52+
let tile_row = local_id.y * RPT;
53+
let tile_col = local_id.x * RPT;
54+
55+
var acc: array<array<f32, 4>, 4>;
56+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
57+
for (var ic: u32 = 0u; ic < RPT; ic = ic + 1u) {
58+
acc[ir][ic] = 0.0;
59+
}
60+
}
61+
62+
let num_tiles = (K + TILE - 1u) / TILE;
63+
for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {
64+
let k_start = t * TILE;
65+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
66+
let arow = local_id.y * RPT + ir;
67+
for (var kk: u32 = 0u; kk < RPT; kk = kk + 1u) {
68+
let col = local_id.x * RPT + kk;
69+
a_sub[arow][col] = read_a(tile_row0 + arow, k_start + col);
70+
b_sub[arow][col] = read_b(k_start + arow, tile_col0 + col);
71+
}
72+
}
73+
workgroupBarrier();
74+
for (var k: u32 = 0u; k < TILE; k = k + 1u) {
75+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
76+
let aval = a_sub[tile_row + ir][k];
77+
for (var ic: u32 = 0u; ic < RPT; ic = ic + 1u) {
78+
acc[ir][ic] = acc[ir][ic] + aval * b_sub[k][tile_col + ic];
79+
}
80+
}
81+
}
82+
workgroupBarrier();
83+
}
84+
85+
let khw = params.KH * params.KW;
86+
let ihw = params.IH * params.IW;
87+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
88+
let m = tile_row0 + tile_row + ir;
89+
if (m >= M) {
90+
continue;
91+
}
92+
let oc = m / khw;
93+
let kh = (m % khw) / params.KW;
94+
let kw = m % params.KW;
95+
var bias_v = 0.0;
96+
if (params.has_bias != 0u) {
97+
bias_v = bias[oc];
98+
}
99+
for (var ic: u32 = 0u; ic < RPT; ic = ic + 1u) {
100+
let n = tile_col0 + tile_col + ic;
101+
if (n < N) {
102+
let b = n / ihw;
103+
let sp = n % ihw;
104+
let oh = (sp / params.IW) * params.KH + kh;
105+
let ow = (sp % params.IW) * params.KW + kw;
106+
out[((b * params.OC + oc) * params.OH + oh) * params.OW + ow] =
107+
acc[ir][ic] + bias_v;
108+
}
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)