Skip to content

Commit 9605465

Browse files
committed
[ExecuTorch][WebGPU] Add reduce op (sum/mean) to the WebGPU backend
Pull Request resolved: #20923 Add an `aten.sum.dim_IntList` / `aten.mean.dim` reduction handler for the training tail. Key changes: - `runtime/ops/reduce/` — per-row reduction WGSL kernel + handler - `CMakeLists.txt` `WEBGPU_SRCS` — wire the source Reuses the shared Vulkan partitioner (`aten.sum.dim_IntList`/`mean.dim` already registered); WebGPU kernel only. Co-authored-with: Claude Code. ghstack-source-id: 405026057 @exported-using-ghexport Differential Revision: [D111755133](https://our.internmc.facebook.com/intern/diff/D111755133/)
1 parent 01409ef commit 9605465

4 files changed

Lines changed: 410 additions & 0 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ set(WEBGPU_SRCS
5656
runtime/ops/log_softmax/LogSoftmax.cpp
5757
runtime/ops/softmax/Softmax.cpp
5858
runtime/ops/bmm/Bmm.cpp
59+
runtime/ops/reduce/Reduce.cpp
5960
runtime/ops/div/BinaryOp.cpp
6061
runtime/ops/sub/BinaryOp.cpp
6162
runtime/ops/linear/Linear.cpp
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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/reduce/reduce_wgsl.h>
13+
14+
#include <webgpu/webgpu.h>
15+
16+
#include <cstdint>
17+
#include <cstring>
18+
#include <stdexcept>
19+
#include <vector>
20+
21+
namespace executorch::backends::webgpu {
22+
23+
namespace {
24+
25+
// Uniform layout matching the WGSL Params struct (16-byte aligned).
26+
struct ReduceParams {
27+
uint32_t outer;
28+
uint32_t r;
29+
uint32_t inner;
30+
uint32_t is_mean;
31+
};
32+
static_assert(sizeof(ReduceParams) == 16, "ReduceParams must be 16 bytes");
33+
34+
void decompose(
35+
const std::vector<int64_t>& dims,
36+
int64_t dim,
37+
uint32_t& outer,
38+
uint32_t& r,
39+
uint32_t& inner) {
40+
const int64_t ndim = static_cast<int64_t>(dims.size());
41+
if (dim < 0) {
42+
dim += ndim;
43+
}
44+
if (ndim == 0 || dim < 0 || dim >= ndim) {
45+
throw std::runtime_error("WebGPU reduce: dim out of range");
46+
}
47+
uint64_t o = 1, in = 1;
48+
for (int64_t d = 0; d < dim; ++d) {
49+
o *= static_cast<uint64_t>(dims[d]);
50+
}
51+
for (int64_t d = dim + 1; d < ndim; ++d) {
52+
in *= static_cast<uint64_t>(dims[d]);
53+
}
54+
outer = static_cast<uint32_t>(o);
55+
r = static_cast<uint32_t>(dims[dim]);
56+
inner = static_cast<uint32_t>(in);
57+
}
58+
59+
void reduce_impl(
60+
WebGPUGraph& graph,
61+
const std::vector<int>& args,
62+
bool is_mean,
63+
const char* op_name) {
64+
const int in_id = args.at(0);
65+
const int dim_id = args.at(1);
66+
const int keepdim_id = args.at(2);
67+
const int out_id = args.at(args.size() - 1);
68+
69+
WGPUDevice device = graph.device();
70+
const auto& in = graph.get_tensor(in_id);
71+
const auto& out = graph.get_tensor(out_id);
72+
73+
bool keepdim = false;
74+
if (graph.get_value_type(keepdim_id) == WebGPUGraph::ValueType::Int) {
75+
keepdim = graph.get_int(keepdim_id) != 0;
76+
}
77+
78+
if (in.dims.empty()) {
79+
throw std::runtime_error("WebGPU reduce: scalar input unsupported");
80+
}
81+
if (graph.get_value_type(dim_id) != WebGPUGraph::ValueType::IntList) {
82+
throw std::runtime_error("WebGPU reduce: dim arg is not an IntList");
83+
}
84+
const std::vector<int64_t>& reduce_dims = graph.get_int_list(dim_id);
85+
// Single-dim reduction only for now; multi-dim is a tracked extension.
86+
if (reduce_dims.size() != 1) {
87+
throw std::runtime_error(
88+
"WebGPU reduce: only single-dim reduction is supported");
89+
}
90+
const int64_t dim = reduce_dims[0];
91+
92+
uint32_t outer = 0, r = 0, inner = 0;
93+
decompose(in.dims, dim, outer, r, inner);
94+
if (outer == 0 || r == 0 || inner == 0) {
95+
throw std::runtime_error("WebGPU reduce: zero-sized reduction");
96+
}
97+
98+
uint64_t in_numel = 1;
99+
for (int64_t d : in.dims) {
100+
in_numel *= static_cast<uint64_t>(d);
101+
}
102+
const uint64_t outputs = static_cast<uint64_t>(outer) * inner;
103+
if (in.nbytes != in_numel * sizeof(float) ||
104+
out.nbytes != outputs * sizeof(float)) {
105+
throw std::runtime_error("WebGPU reduce: fp32-only (byte-size mismatch)");
106+
}
107+
if (outputs > UINT32_MAX) {
108+
throw std::runtime_error(
109+
"WebGPU reduce: output count exceeds dispatch limit");
110+
}
111+
112+
const uint32_t wg_size =
113+
utils::clamp_workgroup_size(device, kReduceWorkgroupSizeX);
114+
// Cooperative reduction: one workgroup per output element (2D-folded grid).
115+
const utils::WgCount workgroup_count = utils::compute_2d_workgroup_count(
116+
device, static_cast<uint32_t>(outputs), 1u, op_name);
117+
118+
ReduceParams params = {};
119+
params.outer = outer;
120+
params.r = r;
121+
params.inner = inner;
122+
params.is_mean = is_mean ? 1u : 0u;
123+
124+
WGPUBufferDescriptor uniform_desc = {};
125+
uniform_desc.size = sizeof(ReduceParams);
126+
uniform_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
127+
uniform_desc.mappedAtCreation = true;
128+
WGPUBuffer uniform_buffer = wgpuDeviceCreateBuffer(device, &uniform_desc);
129+
void* mapped =
130+
wgpuBufferGetMappedRange(uniform_buffer, 0, sizeof(ReduceParams));
131+
std::memcpy(mapped, &params, sizeof(ReduceParams));
132+
wgpuBufferUnmap(uniform_buffer);
133+
graph.add_uniform_buffer_bytes(sizeof(ReduceParams));
134+
135+
WGPUShaderSourceWGSL wgsl_desc = {};
136+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
137+
wgsl_desc.code = {kReduceWGSL, WGPU_STRLEN};
138+
WGPUShaderModuleDescriptor shader_desc = {};
139+
shader_desc.nextInChain = &wgsl_desc.chain;
140+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
141+
142+
WGPUBindGroupLayoutEntry entries[3] = {};
143+
entries[0].binding = 0;
144+
entries[0].visibility = WGPUShaderStage_Compute;
145+
entries[0].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
146+
entries[1].binding = 1;
147+
entries[1].visibility = WGPUShaderStage_Compute;
148+
entries[1].buffer.type = WGPUBufferBindingType_Storage;
149+
entries[2].binding = 2;
150+
entries[2].visibility = WGPUShaderStage_Compute;
151+
entries[2].buffer.type = WGPUBufferBindingType_Uniform;
152+
153+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
154+
bgl_desc.entryCount = 3;
155+
bgl_desc.entries = entries;
156+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
157+
158+
WGPUPipelineLayoutDescriptor pl_desc = {};
159+
pl_desc.bindGroupLayoutCount = 1;
160+
pl_desc.bindGroupLayouts = &bgl;
161+
WGPUPipelineLayout pipeline_layout =
162+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
163+
164+
WGPUConstantEntry wg_size_constant = {};
165+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
166+
wg_size_constant.value = static_cast<double>(wg_size);
167+
168+
WGPUComputePipelineDescriptor pipeline_desc = {};
169+
pipeline_desc.layout = pipeline_layout;
170+
pipeline_desc.compute.module = shader;
171+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
172+
pipeline_desc.compute.constantCount = 1;
173+
pipeline_desc.compute.constants = &wg_size_constant;
174+
WGPUComputePipeline pipeline =
175+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
176+
177+
WGPUBindGroupEntry bg_entries[3] = {};
178+
bg_entries[0].binding = 0;
179+
bg_entries[0].buffer = in.buffer;
180+
bg_entries[0].size = in.nbytes;
181+
bg_entries[1].binding = 1;
182+
bg_entries[1].buffer = out.buffer;
183+
bg_entries[1].size = out.nbytes;
184+
bg_entries[2].binding = 2;
185+
bg_entries[2].buffer = uniform_buffer;
186+
bg_entries[2].size = sizeof(ReduceParams);
187+
188+
WGPUBindGroupDescriptor bg_desc = {};
189+
bg_desc.layout = bgl;
190+
bg_desc.entryCount = 3;
191+
bg_desc.entries = bg_entries;
192+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
193+
194+
const size_t dispatch_idx = graph.add_dispatch(
195+
{pipeline, bind_group, workgroup_count.x, op_name, workgroup_count.y});
196+
197+
// Dynamic shapes: recompute the decomposition for the reduced dim + dispatch.
198+
WGPUBuffer params_buf = uniform_buffer;
199+
const uint32_t is_mean_u = is_mean ? 1u : 0u;
200+
const uint64_t build_outputs = outputs;
201+
graph.add_tensor_resize_hook(
202+
in_id,
203+
[in_id,
204+
out_id,
205+
dim,
206+
keepdim,
207+
is_mean_u,
208+
build_outputs,
209+
dispatch_idx,
210+
params_buf](WebGPUGraph& g) {
211+
const auto& d = g.cur_dims(in_id);
212+
uint32_t o = 0, rr = 0, n = 0;
213+
decompose(std::vector<int64_t>(d.begin(), d.end()), dim, o, rr, n);
214+
if (o == 0u || rr == 0u || n == 0u) {
215+
throw std::runtime_error("WebGPU reduce: live zero-sized reduction");
216+
}
217+
const uint64_t live_outputs = static_cast<uint64_t>(o) * n;
218+
if (live_outputs > build_outputs) {
219+
throw std::runtime_error(
220+
"WebGPU reduce: live output count exceeds build max");
221+
}
222+
ReduceParams p = {};
223+
p.outer = o;
224+
p.r = rr;
225+
p.inner = n;
226+
p.is_mean = is_mean_u;
227+
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
228+
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
229+
g.device(),
230+
static_cast<uint32_t>(live_outputs),
231+
1u,
232+
"reduce(resize)");
233+
g.dispatch_at(dispatch_idx).workgroup_count_x = wgc.x;
234+
g.dispatch_at(dispatch_idx).workgroup_count_y = wgc.y;
235+
// Propagate reduced output dims for downstream resize hooks.
236+
int64_t nd = static_cast<int64_t>(d.size());
237+
int64_t rd = dim < 0 ? dim + nd : dim;
238+
std::vector<int64_t> od;
239+
for (int64_t i = 0; i < nd; ++i) {
240+
if (i == rd) {
241+
if (keepdim) {
242+
od.push_back(1);
243+
}
244+
} else {
245+
od.push_back(d[i]);
246+
}
247+
}
248+
g.set_cur_dims(out_id, od);
249+
});
250+
251+
wgpuShaderModuleRelease(shader);
252+
wgpuBindGroupLayoutRelease(bgl);
253+
wgpuPipelineLayoutRelease(pipeline_layout);
254+
// Graph owns it so the resize hook can rewrite it; freed in the dtor.
255+
graph.own_uniform_buffer(uniform_buffer);
256+
}
257+
258+
void sum_dim_impl(WebGPUGraph& graph, const std::vector<int>& args) {
259+
reduce_impl(graph, args, /*is_mean=*/false, "sum.dim_IntList");
260+
}
261+
262+
void mean_dim_impl(WebGPUGraph& graph, const std::vector<int>& args) {
263+
reduce_impl(graph, args, /*is_mean=*/true, "mean.dim");
264+
}
265+
266+
} // namespace
267+
268+
WEBGPU_REGISTER_OPERATORS {
269+
WEBGPU_REGISTER_OP(aten.sum.dim_IntList, sum_dim_impl);
270+
WEBGPU_REGISTER_OP(aten.mean.dim, mean_dim_impl);
271+
}
272+
273+
} // namespace executorch::backends::webgpu
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
struct Params {
2+
outer_: u32,
3+
r_: u32,
4+
inner_: u32,
5+
is_mean: u32,
6+
};
7+
8+
@group(0) @binding(0) var<storage, read> inp: array<f32>;
9+
@group(0) @binding(1) var<storage, read_write> out: array<f32>;
10+
@group(0) @binding(2) var<uniform> params: Params;
11+
12+
override wg_size: u32 = 256;
13+
14+
// Cooperative shared-memory reduction, one workgroup per output element: each
15+
// thread sums a strided slice of the reduced dim into a shared partial, then
16+
// thread 0 folds the partials. Same one-workgroup-per-row shared-memory shape as
17+
// Vulkan's reduce_per_row_buffer.glsl. Fixed 256 upper bound >= any clamped
18+
// wg_size; only [0, wg_size) is used.
19+
var<workgroup> partials: array<f32, 256>;
20+
21+
@compute @workgroup_size(wg_size)
22+
fn main(
23+
@builtin(workgroup_id) wid: vec3<u32>,
24+
@builtin(local_invocation_id) lid: vec3<u32>,
25+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
26+
// One workgroup per output; 2D-fold lifts the 65535 grid cap. `t` is uniform
27+
// across the workgroup, so the early return keeps the barrier in uniform flow.
28+
let t = wid.x + wid.y * num_workgroups.x;
29+
let outs = params.outer_ * params.inner_;
30+
if (t >= outs) {
31+
return;
32+
}
33+
let oo = t / params.inner_;
34+
let ii = t % params.inner_;
35+
let base = oo * params.r_ * params.inner_ + ii;
36+
37+
var acc: f32 = 0.0;
38+
var k: u32 = lid.x;
39+
while (k < params.r_) {
40+
acc = acc + inp[base + k * params.inner_];
41+
k = k + wg_size;
42+
}
43+
partials[lid.x] = acc;
44+
workgroupBarrier();
45+
46+
if (lid.x == 0u) {
47+
var s: f32 = partials[0];
48+
for (var i: u32 = 1u; i < wg_size; i = i + 1u) {
49+
s = s + partials[i];
50+
}
51+
if (params.is_mean == 1u) {
52+
s = s / f32(params.r_);
53+
}
54+
out[t] = s;
55+
}
56+
}

0 commit comments

Comments
 (0)