Skip to content

Commit 5d527f1

Browse files
committed
[ExecuTorch][WebGPU] Add fp32 matmul ops (mm, bmm, linear) to the WebGPU backend
Pull Request resolved: #20917 Add fp32 GEMM handlers (`aten.mm`, `aten.bmm`, `aten.linear`) — the dense matmuls the on-device training tail needs (tiled + vec4 WGSL). Key changes: - `runtime/ops/{mm,bmm,linear}/` — tiled + vec4 fp32 GEMM WGSL kernels + handlers - `CMakeLists.txt` `WEBGPU_SRCS` — wire the three sources Reuses the shared Vulkan partitioner (`aten.mm`/`bmm`/`linear` already have Vulkan `OpFeatures`); this adds the WebGPU kernels only. Co-authored-with: Claude Code. ghstack-source-id: 405026038 @exported-using-ghexport Differential Revision: [D111755135](https://our.internmc.facebook.com/intern/diff/D111755135/)
1 parent 21554e5 commit 5d527f1

18 files changed

Lines changed: 1855 additions & 0 deletions

backends/webgpu/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ set(WEBGPU_SRCS
5252
runtime/ops/cat/Cat.cpp
5353
runtime/ops/index/Index.cpp
5454
runtime/ops/sdpa_fd_decode/SdpaFdDecode.cpp
55+
runtime/ops/mm/Mm.cpp
56+
runtime/ops/bmm/Bmm.cpp
57+
runtime/ops/linear/Linear.cpp
5558
)
5659

5760
add_library(webgpu_backend ${WEBGPU_SRCS})
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
10+
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
11+
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
12+
#include <executorch/backends/webgpu/runtime/ops/bmm/bmm_tiled_wgsl.h>
13+
#include <executorch/backends/webgpu/runtime/ops/bmm/bmm_vec4_wgsl.h>
14+
15+
#include <webgpu/webgpu.h>
16+
17+
#include <cstdint>
18+
#include <stdexcept>
19+
#include <vector>
20+
21+
namespace executorch::backends::webgpu {
22+
23+
namespace {
24+
25+
struct BmmParams {
26+
uint32_t B;
27+
uint32_t M;
28+
uint32_t N;
29+
uint32_t K;
30+
};
31+
static_assert(sizeof(BmmParams) == 16, "BmmParams must be 16 bytes");
32+
33+
constexpr uint32_t kTile = 32u;
34+
35+
// Batched shared-memory tiled GEMM.
36+
void bmm_impl(WebGPUGraph& graph, const std::vector<int>& args) {
37+
const int a_id = args.at(0);
38+
const int b_id = args.at(1);
39+
const int out_id = args.at(2);
40+
41+
WGPUDevice device = graph.device();
42+
43+
const auto& a = graph.get_tensor(a_id);
44+
const auto& b = graph.get_tensor(b_id);
45+
const auto& out = graph.get_tensor(out_id);
46+
47+
if (a.dims.size() != 3 || b.dims.size() != 3) {
48+
throw std::runtime_error("WebGPU bmm: inputs must be 3D");
49+
}
50+
const uint32_t B = static_cast<uint32_t>(a.dims[0]);
51+
const uint32_t M = static_cast<uint32_t>(a.dims[1]);
52+
const uint32_t K = static_cast<uint32_t>(a.dims[2]);
53+
const uint32_t N = static_cast<uint32_t>(b.dims[2]);
54+
if (static_cast<uint32_t>(b.dims[0]) != B ||
55+
static_cast<uint32_t>(b.dims[1]) != K) {
56+
throw std::runtime_error("WebGPU bmm: batch/K mismatch between a and b");
57+
}
58+
if (B == 0 || M == 0 || N == 0 || K == 0) {
59+
throw std::runtime_error("WebGPU bmm: B, M, N, or K == 0");
60+
}
61+
62+
const uint64_t outputs =
63+
static_cast<uint64_t>(B) * static_cast<uint64_t>(M) * N;
64+
if (a.nbytes != static_cast<uint64_t>(B) * M * K * sizeof(float) ||
65+
b.nbytes != static_cast<uint64_t>(B) * K * N * sizeof(float) ||
66+
out.nbytes != outputs * sizeof(float)) {
67+
throw std::runtime_error("WebGPU bmm: fp32-only (byte-size mismatch)");
68+
}
69+
70+
const uint32_t max_wg = utils::queried_max_workgroups(device);
71+
const uint32_t dispatch_x = (N + kTile - 1u) / kTile;
72+
// uint64 so B * ceil(M/32) can't wrap before the limit check.
73+
const uint64_t dispatch_y64 =
74+
static_cast<uint64_t>(B) * ((M + kTile - 1u) / kTile);
75+
if (dispatch_x > max_wg || dispatch_y64 > max_wg) {
76+
throw std::runtime_error("WebGPU bmm: tile grid exceeds dispatch limit");
77+
}
78+
const uint32_t dispatch_y = static_cast<uint32_t>(dispatch_y64);
79+
80+
BmmParams params = {};
81+
params.B = B;
82+
params.M = M;
83+
params.N = N;
84+
params.K = K;
85+
86+
WGPUBuffer uniform_buffer =
87+
utils::make_uniform(device, &params, sizeof(BmmParams));
88+
graph.add_uniform_buffer_bytes(sizeof(BmmParams));
89+
90+
// vec4 path when K and N are multiples of 4 (wider 16B loads); else scalar.
91+
const bool use_vec4 = (K % 4u == 0u) && (N % 4u == 0u);
92+
WGPUShaderSourceWGSL wgsl_desc = {};
93+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
94+
wgsl_desc.code = {use_vec4 ? kBmmVec4WGSL : kBmmTiledWGSL, WGPU_STRLEN};
95+
WGPUShaderModuleDescriptor shader_desc = {};
96+
shader_desc.nextInChain = &wgsl_desc.chain;
97+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
98+
99+
WGPUBindGroupLayoutEntry entries[4] = {};
100+
entries[0].binding = 0;
101+
entries[0].visibility = WGPUShaderStage_Compute;
102+
entries[0].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
103+
entries[1].binding = 1;
104+
entries[1].visibility = WGPUShaderStage_Compute;
105+
entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
106+
entries[2].binding = 2;
107+
entries[2].visibility = WGPUShaderStage_Compute;
108+
entries[2].buffer.type = WGPUBufferBindingType_Storage;
109+
entries[3].binding = 3;
110+
entries[3].visibility = WGPUShaderStage_Compute;
111+
entries[3].buffer.type = WGPUBufferBindingType_Uniform;
112+
113+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
114+
bgl_desc.entryCount = 4;
115+
bgl_desc.entries = entries;
116+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
117+
118+
WGPUPipelineLayoutDescriptor pl_desc = {};
119+
pl_desc.bindGroupLayoutCount = 1;
120+
pl_desc.bindGroupLayouts = &bgl;
121+
WGPUPipelineLayout pipeline_layout =
122+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
123+
124+
// Tiled kernel has a fixed @workgroup_size(8, 8, 1) — no override constant.
125+
WGPUComputePipelineDescriptor pipeline_desc = {};
126+
pipeline_desc.layout = pipeline_layout;
127+
pipeline_desc.compute.module = shader;
128+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
129+
WGPUComputePipeline pipeline =
130+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
131+
132+
WGPUBindGroupEntry bg_entries[4] = {};
133+
bg_entries[0].binding = 0;
134+
bg_entries[0].buffer = a.buffer;
135+
bg_entries[0].size = a.nbytes;
136+
bg_entries[1].binding = 1;
137+
bg_entries[1].buffer = b.buffer;
138+
bg_entries[1].size = b.nbytes;
139+
bg_entries[2].binding = 2;
140+
bg_entries[2].buffer = out.buffer;
141+
bg_entries[2].size = out.nbytes;
142+
bg_entries[3].binding = 3;
143+
bg_entries[3].buffer = uniform_buffer;
144+
bg_entries[3].size = sizeof(BmmParams);
145+
146+
WGPUBindGroupDescriptor bg_desc = {};
147+
bg_desc.layout = bgl;
148+
bg_desc.entryCount = 4;
149+
bg_desc.entries = bg_entries;
150+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
151+
152+
WebGPUDispatch dispatch;
153+
dispatch.pipeline = pipeline;
154+
dispatch.bind_group = bind_group;
155+
dispatch.workgroup_count_x = dispatch_x;
156+
dispatch.workgroup_count_y = dispatch_y;
157+
dispatch.kernel_name = "bmm";
158+
const size_t dispatch_idx = graph.add_dispatch(dispatch);
159+
160+
// Dynamic shapes: re-derive B, M (from a) and N (from b) + the tile grid.
161+
WGPUBuffer params_buf = uniform_buffer;
162+
graph.add_tensor_resize_hook(
163+
a_id,
164+
[a_id, b_id, out_id, B, M, N, K, use_vec4, dispatch_idx, params_buf](
165+
WebGPUGraph& g) {
166+
const auto& da = g.cur_dims(a_id);
167+
const auto& db = g.cur_dims(b_id);
168+
if (da.size() != 3 || db.size() != 3 ||
169+
static_cast<uint32_t>(da[2]) != K ||
170+
static_cast<uint32_t>(db[1]) != K ||
171+
static_cast<uint32_t>(db[0]) != static_cast<uint32_t>(da[0])) {
172+
throw std::runtime_error("WebGPU bmm: live input dims invalid");
173+
}
174+
const uint32_t live_b = static_cast<uint32_t>(da[0]);
175+
const uint32_t live_m = static_cast<uint32_t>(da[1]);
176+
const uint32_t live_n = static_cast<uint32_t>(db[2]);
177+
if (live_b == 0u || live_m == 0u || live_n == 0u) {
178+
throw std::runtime_error("WebGPU bmm: live B, M, or N == 0");
179+
}
180+
if (live_b > B || live_m > M || live_n > N) {
181+
throw std::runtime_error(
182+
"WebGPU bmm: live dims exceed build-time max");
183+
}
184+
// vec4 baked for N%4==0; a live N breaking it reads past the row.
185+
if (use_vec4 && live_n % 4u != 0u) {
186+
throw std::runtime_error("WebGPU bmm: live N breaks vec4 alignment");
187+
}
188+
BmmParams p = {};
189+
p.B = live_b;
190+
p.M = live_m;
191+
p.N = live_n;
192+
p.K = K;
193+
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
194+
const uint32_t max_wg = utils::queried_max_workgroups(g.device());
195+
const uint32_t dx = (live_n + kTile - 1u) / kTile;
196+
const uint64_t dy64 =
197+
static_cast<uint64_t>(live_b) * ((live_m + kTile - 1u) / kTile);
198+
if (dx > max_wg || dy64 > max_wg) {
199+
throw std::runtime_error(
200+
"WebGPU bmm(resize): tile grid exceeds dispatch limit");
201+
}
202+
g.dispatch_at(dispatch_idx).workgroup_count_x = dx;
203+
g.dispatch_at(dispatch_idx).workgroup_count_y =
204+
static_cast<uint32_t>(dy64);
205+
g.set_cur_dims(
206+
out_id,
207+
{static_cast<int64_t>(live_b),
208+
static_cast<int64_t>(live_m),
209+
static_cast<int64_t>(live_n)});
210+
});
211+
212+
wgpuShaderModuleRelease(shader);
213+
wgpuBindGroupLayoutRelease(bgl);
214+
wgpuPipelineLayoutRelease(pipeline_layout);
215+
graph.own_uniform_buffer(uniform_buffer);
216+
}
217+
218+
} // namespace
219+
220+
WEBGPU_REGISTER_OPERATORS {
221+
WEBGPU_REGISTER_OP(aten.bmm.default, bmm_impl);
222+
}
223+
224+
} // namespace executorch::backends::webgpu
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
struct Params {
2+
B: u32,
3+
M: u32,
4+
N: u32,
5+
K: u32,
6+
};
7+
8+
@group(0) @binding(0) var<storage, read> a: array<f32>;
9+
@group(0) @binding(1) var<storage, read> b: array<f32>;
10+
@group(0) @binding(2) var<storage, read_write> out: array<f32>;
11+
@group(0) @binding(3) var<uniform> params: Params;
12+
13+
const TILE: u32 = 32u;
14+
const RPT: u32 = 4u;
15+
16+
var<workgroup> a_sub: array<array<f32, 32>, 32>;
17+
var<workgroup> b_sub: array<array<f32, 32>, 32>;
18+
19+
fn read_a(base: u32, row: u32, col: u32) -> f32 {
20+
if (row < params.M && col < params.K) {
21+
return a[base + row * params.K + col];
22+
}
23+
return 0.0;
24+
}
25+
26+
fn read_b(base: u32, krow: u32, col: u32) -> f32 {
27+
if (krow < params.K && col < params.N) {
28+
return b[base + krow * params.N + col];
29+
}
30+
return 0.0;
31+
}
32+
33+
@compute @workgroup_size(8, 8, 1)
34+
fn main(
35+
@builtin(workgroup_id) wg_id: vec3<u32>,
36+
@builtin(local_invocation_id) local_id: vec3<u32>) {
37+
let tiles_per_m = (params.M + TILE - 1u) / TILE;
38+
let bn = wg_id.y / tiles_per_m;
39+
let row_tile = wg_id.y % tiles_per_m;
40+
let tile_row0 = row_tile * TILE;
41+
let tile_col0 = wg_id.x * TILE;
42+
let tile_row = local_id.y * RPT;
43+
let tile_col = local_id.x * RPT;
44+
45+
let a_base = bn * params.M * params.K;
46+
let b_base = bn * params.K * params.N;
47+
let out_base = bn * params.M * params.N;
48+
49+
var acc: array<array<f32, 4>, 4>;
50+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
51+
for (var ic: u32 = 0u; ic < RPT; ic = ic + 1u) {
52+
acc[ir][ic] = 0.0;
53+
}
54+
}
55+
56+
let num_tiles = (params.K + TILE - 1u) / TILE;
57+
for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {
58+
let k_start = t * TILE;
59+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
60+
let arow = local_id.y * RPT + ir;
61+
for (var kk: u32 = 0u; kk < RPT; kk = kk + 1u) {
62+
let col = local_id.x * RPT + kk;
63+
a_sub[arow][col] = read_a(a_base, tile_row0 + arow, k_start + col);
64+
b_sub[arow][col] = read_b(b_base, k_start + arow, tile_col0 + col);
65+
}
66+
}
67+
workgroupBarrier();
68+
69+
for (var k: u32 = 0u; k < TILE; k = k + 1u) {
70+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
71+
let aval = a_sub[tile_row + ir][k];
72+
for (var ic: u32 = 0u; ic < RPT; ic = ic + 1u) {
73+
acc[ir][ic] = acc[ir][ic] + aval * b_sub[k][tile_col + ic];
74+
}
75+
}
76+
}
77+
workgroupBarrier();
78+
}
79+
80+
for (var ir: u32 = 0u; ir < RPT; ir = ir + 1u) {
81+
for (var ic: u32 = 0u; ic < RPT; ic = ic + 1u) {
82+
let r = tile_row0 + tile_row + ir;
83+
let c = tile_col0 + tile_col + ic;
84+
if (r < params.M && c < params.N) {
85+
out[out_base + r * params.N + c] = acc[ir][ic];
86+
}
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)