Skip to content

Commit 186c6d1

Browse files
authored
[ExecuTorch][WebGPU] Use 2D-folded dispatch for cat and slice (pytorch#23900)
cat and slice dispatched a 1D grid with one invocation per element, so any input (cat) or output (slice) larger than 65535 workgroups failed to load even though the buffers fit comfortably within device limits. Switch both to compute_2d_workgroup_count and the folded flat index the other elementwise ops already use, including in the resize hooks. This change was authored with Claude Code. Test Plan: - New heavy grid_limit op tests for cat and slice cross the 1D limit; they fail to load before this change and pass after, alongside the existing cat/slice cases (native Dawn on Metal). - gen_wgsl_headers.py --check, test_wgsl_codegen.py, lintrunner.
1 parent 2a84702 commit 186c6d1

8 files changed

Lines changed: 68 additions & 26 deletions

File tree

‎backends/webgpu/runtime/ops/cat/Cat.cpp‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ void cat_impl(WebGPUGraph& graph, const std::vector<int>& args) {
7171

7272
// Validate + cache input meta/wgc BEFORE any GPU alloc (no leak on throw).
7373
std::vector<TensorMeta> in_metas(ids.size());
74-
std::vector<uint32_t> wg_counts(ids.size());
74+
std::vector<utils::WgCount> wg_counts(ids.size());
7575
int64_t concat_sum = 0;
7676
for (size_t k = 0; k < ids.size(); k++) {
7777
const int id = ids[k];
@@ -92,7 +92,7 @@ void cat_impl(WebGPUGraph& graph, const std::vector<int>& args) {
9292
static_cast<size_t>(in_metas[k].numel) * sizeof(float)) {
9393
throw std::runtime_error("cat: non-fp32 input (nbytes != numel * 4)");
9494
}
95-
wg_counts[k] = utils::compute_1d_workgroup_count(
95+
wg_counts[k] = utils::compute_2d_workgroup_count(
9696
device, in_metas[k].numel, wg_size, "cat");
9797
concat_sum += in_tensor.dims[dim];
9898
}
@@ -158,8 +158,12 @@ void cat_impl(WebGPUGraph& graph, const std::vector<int>& args) {
158158

159159
in_meta_bufs[k] = in_meta_buf;
160160
params_bufs[k] = params_buf;
161-
dispatch_idxs[k] =
162-
graph.add_dispatch({bundle.pipeline, bundle.bind_group, wg_counts[k]});
161+
dispatch_idxs[k] = graph.add_dispatch(
162+
{bundle.pipeline,
163+
bundle.bind_group,
164+
wg_counts[k].x,
165+
"cat",
166+
wg_counts[k].y});
163167
if (!shared_resources.has_value()) {
164168
shared_resources.emplace(std::move(bundle));
165169
}
@@ -211,9 +215,10 @@ void cat_impl(WebGPUGraph& graph, const std::vector<int>& args) {
211215
params.off_k = off;
212216
wgpuQueueWriteBuffer(
213217
g.queue(), params_bufs[k], 0, &params, sizeof(params));
214-
g.dispatch_at(dispatch_idxs[k]).workgroup_count_x =
215-
utils::compute_1d_workgroup_count(
216-
g.device(), in_meta.numel, wg_size, "cat(resize)");
218+
const utils::WgCount wg = utils::compute_2d_workgroup_count(
219+
g.device(), in_meta.numel, wg_size, "cat(resize)");
220+
g.dispatch_at(dispatch_idxs[k]).workgroup_count_x = wg.x;
221+
g.dispatch_at(dispatch_idxs[k]).workgroup_count_y = wg.y;
217222
off += static_cast<uint32_t>(in_dims[cdim]);
218223
}
219224
};

‎backends/webgpu/runtime/ops/cat/cat.wgsl‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ struct Params {
1919
override wg_size: u32 = 64u;
2020

2121
@compute @workgroup_size(wg_size, 1, 1)
22-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
23-
let in_bufi = gid.x;
22+
fn main(
23+
@builtin(global_invocation_id) gid: vec3<u32>,
24+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
25+
// 2D-folded flat index (lifts the 65535 1D-dispatch cap for large tensors).
26+
let in_bufi = gid.x + gid.y * (num_workgroups.x * wg_size);
2427
if (in_bufi >= in_meta.numel) {
2528
return;
2629
}

‎backends/webgpu/runtime/ops/cat/cat_wgsl.h‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace executorch::backends::webgpu {
1414

1515
// @generated from cat.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: 1a5f66607e2959c12d757989a3c1ae6fc6f2ede139bc9afb6e14dd3961a6fcb7
16+
// wgsl-sha256: 5bef4284287c6d3b7f7aefcbaf642f96347575f5271dd190975a6d59d093af36
1717
inline constexpr const char* kCatWGSL = R"(
1818
@group(0) @binding(0) var<storage, read> input: array<f32>;
1919
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
@@ -36,8 +36,11 @@ struct Params {
3636
override wg_size: u32 = 64u;
3737
3838
@compute @workgroup_size(wg_size, 1, 1)
39-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
40-
let in_bufi = gid.x;
39+
fn main(
40+
@builtin(global_invocation_id) gid: vec3<u32>,
41+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
42+
// 2D-folded flat index (lifts the 65535 1D-dispatch cap for large tensors).
43+
let in_bufi = gid.x + gid.y * (num_workgroups.x * wg_size);
4144
if (in_bufi >= in_meta.numel) {
4245
return;
4346
}

‎backends/webgpu/runtime/ops/slice/Slice.cpp‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ void slice_impl(WebGPUGraph& graph, const std::vector<int>& args) {
152152
params.step = static_cast<uint32_t>(step);
153153

154154
uint32_t wg_size = utils::clamp_workgroup_size(device, kSliceWorkgroupSizeX);
155-
uint32_t workgroup_count = utils::compute_1d_workgroup_count(
155+
const utils::WgCount workgroup_count = utils::compute_2d_workgroup_count(
156156
device, out_meta.numel, wg_size, "slice");
157157

158158
WGPUConstantEntry wg_size_constant = {};
@@ -186,7 +186,12 @@ void slice_impl(WebGPUGraph& graph, const std::vector<int>& args) {
186186
&wg_size_constant,
187187
1);
188188

189-
graph.add_dispatch({bundle.pipeline, bundle.bind_group, workgroup_count});
189+
graph.add_dispatch(
190+
{bundle.pipeline,
191+
bundle.bind_group,
192+
workgroup_count.x,
193+
"slice",
194+
workgroup_count.y});
190195
const size_t dispatch_idx = graph.num_dispatches() - 1;
191196

192197
// Dynamic shapes: live start/end -> out[dim] len + meta/params/dispatch.
@@ -228,9 +233,10 @@ void slice_impl(WebGPUGraph& graph, const std::vector<int>& args) {
228233
p.start = static_cast<uint32_t>(start);
229234
p.step = static_cast<uint32_t>(step);
230235
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
231-
g.dispatch_at(dispatch_idx).workgroup_count_x =
232-
utils::compute_1d_workgroup_count(
233-
g.device(), om.numel, wg_size, "slice(resize)");
236+
const utils::WgCount wg = utils::compute_2d_workgroup_count(
237+
g.device(), om.numel, wg_size, "slice(resize)");
238+
g.dispatch_at(dispatch_idx).workgroup_count_x = wg.x;
239+
g.dispatch_at(dispatch_idx).workgroup_count_y = wg.y;
234240
};
235241
if (is_symint(graph, start_id)) {
236242
graph.add_resize_hook(start_id, recompute);

‎backends/webgpu/runtime/ops/slice/slice.wgsl‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ struct Params {
2020
override wg_size: u32 = 64u;
2121

2222
@compute @workgroup_size(wg_size, 1, 1)
23-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
24-
let out_bufi = gid.x;
23+
fn main(
24+
@builtin(global_invocation_id) gid: vec3<u32>,
25+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
26+
// 2D-folded flat index (lifts the 65535 1D-dispatch cap for large tensors).
27+
let out_bufi = gid.x + gid.y * (num_workgroups.x * wg_size);
2528
if (out_bufi >= out_meta.numel) {
2629
return;
2730
}

‎backends/webgpu/runtime/ops/slice/slice_wgsl.h‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace executorch::backends::webgpu {
1414

1515
// @generated from slice.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: 6a895a8c321cd3ddaffc468d7843c9dea3eaeb9d3de0088a1a09419b3ccfd10b
16+
// wgsl-sha256: eb05f6d85e338fa3bb3dcf485e75a7fde71171fdb98098b08b235ed2bf009113
1717
inline constexpr const char* kSliceWGSL = R"(
1818
@group(0) @binding(0) var<storage, read> input: array<f32>;
1919
@group(0) @binding(1) var<storage, read_write> output: array<f32>;
@@ -37,8 +37,11 @@ struct Params {
3737
override wg_size: u32 = 64u;
3838
3939
@compute @workgroup_size(wg_size, 1, 1)
40-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
41-
let out_bufi = gid.x;
40+
fn main(
41+
@builtin(global_invocation_id) gid: vec3<u32>,
42+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
43+
// 2D-folded flat index (lifts the 65535 1D-dispatch cap for large tensors).
44+
let out_bufi = gid.x + gid.y * (num_workgroups.x * wg_size);
4245
if (out_bufi >= out_meta.numel) {
4346
return;
4447
}

‎backends/webgpu/test/op_tests/cases.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,17 @@ def _unsqueeze_suite() -> WebGPUTestSuite:
10521052

10531053
@register_op_test("slice")
10541054
def _slice_suite() -> WebGPUTestSuite:
1055-
return _fn_config_suite(SliceModule, _SLICE_CONFIGS)
1055+
suite = _fn_config_suite(SliceModule, _SLICE_CONFIGS)
1056+
# Output past the 65535 x 64 1D dispatch limit -> 2D-folded grid.
1057+
suite.cases.append(
1058+
Case(
1059+
name="grid_limit",
1060+
construct={"fn": lambda x: x[:, :1]},
1061+
inputs=((1, 2, 2100, 2100),),
1062+
heavy=True,
1063+
)
1064+
)
1065+
return suite
10561066

10571067

10581068
@register_op_test("permute")
@@ -1076,6 +1086,15 @@ def _cat_suite() -> WebGPUTestSuite:
10761086
cases=[
10771087
Case(name=n, construct={"dim": dim}, inputs=tuple(shapes))
10781088
for n, (shapes, dim) in _CAT_CONFIGS.items()
1089+
]
1090+
+ [
1091+
# Per-input dispatch past the 65535 x 64 1D limit -> 2D-folded grid.
1092+
Case(
1093+
name="grid_limit",
1094+
construct={"dim": 1},
1095+
inputs=((1, 1, 2100, 2100), (1, 1, 2100, 2100)),
1096+
heavy=True,
1097+
)
10791098
],
10801099
golden_dtype="float32", # concatenation copies values; fp64 bit-identical
10811100
)

‎backends/webgpu/test/test_wgsl_codegen.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,14 @@ def test_generated_output_manifest_digest(self) -> None:
220220
digest.update(b"\0")
221221
digest.update(output.read_bytes())
222222
digest.update(b"\0")
223-
self.assertEqual(len(outputs), 136)
223+
self.assertEqual(len(outputs), 138)
224224
self.assertEqual(
225225
digest.hexdigest(),
226-
"0512f8d258952e446ffaedcb653b6a3a720eccf8a6b5327d95fd454a912214a3",
226+
"fc01291b581fcbbb1112ed5b0c7ce5fcab2e1476d2ea92018ae300c9495cf132",
227227
)
228228
self.assertEqual(
229229
hashlib.sha256(g.registry_path().read_bytes()).hexdigest(),
230-
"28aaa7a8d3e916df43e407120e91d487d0d51cbc5ca93c56bd822d25d109890e",
230+
"3f5a3acd2645ea422a14091daa8fc2b7e5163d2c93aee06aed3bc0d2aa1d43d4",
231231
)
232232

233233
def test_rope_hf_reconstructs_full_2d_grid_stride(self) -> None:

0 commit comments

Comments
 (0)