Skip to content

Commit 1fb8b57

Browse files
authored
[ExecuTorch][WebGPU] Promote unary op plumbing to a shared 2D-dispatch helper (#21297)
1 parent c6f012c commit 1fb8b57

235 files changed

Lines changed: 17816 additions & 1630 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backends/test/suite/flows/webgpu.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,9 @@ def _create_webgpu_flow() -> TestFlow:
1616
skip_patterns=[
1717
"float16",
1818
"float64", # Not supported in swiftshader
19-
# WebGPU add is elementwise-only; broadcasting add.Tensor unsupported.
20-
"bcast_first",
21-
"bcast_second",
2219
"hardswish",
2320
"lstm_batch_sizes",
2421
"upsample_nearest2d",
25-
# torchvision models with broadcasting adds; resnet50 covers wide.
26-
"mobilenet_v3_small",
27-
"shufflenet_v2_x1_0",
28-
"resnet50",
29-
"vit_b_16",
30-
"swin_v2_t",
31-
"convnext_small",
3222
],
3323
)
3424

backends/webgpu/runtime/WebGPUBackend.cpp

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,22 @@ Error WebGPUBackend::execute(
164164
return Error::Internal;
165165
}
166166

167-
// Execute the compute graph
168-
graph->execute();
169-
170-
// Copy outputs from GPU staging buffers to EValue tensor data pointers
171-
std::vector<std::pair<void*, size_t>> outputs;
172-
outputs.reserve(num_outputs);
173-
for (size_t i = 0; i < num_outputs; i++) {
174-
const size_t arg_idx = num_inputs + i;
175-
auto& tensor = args[arg_idx]->toTensor();
176-
outputs.emplace_back(tensor.mutable_data_ptr(), tensor.nbytes());
167+
// Execute + read back; fail loud as a runtime Error so a throw never crosses
168+
// the backend boundary.
169+
try {
170+
graph->execute();
171+
std::vector<std::pair<void*, size_t>> outputs;
172+
outputs.reserve(num_outputs);
173+
for (size_t i = 0; i < num_outputs; i++) {
174+
const size_t arg_idx = num_inputs + i;
175+
auto& tensor = args[arg_idx]->toTensor();
176+
outputs.emplace_back(tensor.mutable_data_ptr(), tensor.nbytes());
177+
}
178+
graph->copy_outputs(outputs);
179+
} catch (const std::exception& e) {
180+
ET_LOG(Error, "WebGPU execute / output copy failed: %s", e.what());
181+
return Error::Internal;
177182
}
178-
graph->copy_outputs(outputs);
179183

180184
return Error::Ok;
181185
}

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ void WebGPUGraph::build(
508508
}
509509
tensor.elem_size = vk_datatype_size(vk_tensor->datatype());
510510
tensor.is_int = vk_datatype_is_int(vk_tensor->datatype());
511+
tensor.is_int8 = vk_tensor->datatype() == vkgraph::VkDataType::INT8;
511512
tensor.nbytes = numel * tensor.elem_size;
512513
// Live dims start == max (serialized upper bound); resize_input shrinks
513514
// them per call. Static graphs keep cur == max forever.
@@ -1902,9 +1903,12 @@ void WebGPUGraph::copy_outputs(std::vector<std::pair<void*, size_t>>& outputs) {
19021903

19031904
std::vector<MapCallbackData> cb_data(count);
19041905
std::vector<WGPUFuture> map_futures(count, WGPUFuture{});
1906+
// Map each output's LIVE staging size (an int64 output is int32-backed).
1907+
std::vector<size_t> map_nbytes(count, 0);
19051908

19061909
for (size_t i = 0; i < count; i++) {
1907-
if (outputs[i].second == 0) {
1910+
map_nbytes[i] = tensors_[output_ids_[i]].cur_nbytes;
1911+
if (map_nbytes[i] == 0) {
19081912
cb_data[i].status = WGPUMapAsyncStatus_Success;
19091913
continue;
19101914
}
@@ -1916,29 +1920,62 @@ void WebGPUGraph::copy_outputs(std::vector<std::pair<void*, size_t>>& outputs) {
19161920
output_staging_buffers_[i],
19171921
WGPUMapMode_Read,
19181922
0,
1919-
outputs[i].second,
1923+
map_nbytes[i],
19201924
cb_info);
19211925
}
19221926

1923-
for (size_t i = 0; i < count; i++) {
1924-
if (outputs[i].second != 0 &&
1925-
webgpu_wait(instance_, map_futures[i]) != WGPUWaitStatus_Success) {
1926-
throw std::runtime_error("WebGPU: WaitAny failed for output map");
1927-
}
1928-
}
1927+
// Tracks which output buffers are currently mapped so a mid-loop throw can
1928+
// release them before propagating (no dangling mapped buffers).
1929+
std::vector<bool> is_mapped(count, false);
19291930

1930-
for (size_t i = 0; i < count; i++) {
1931-
if (outputs[i].second == 0) {
1932-
continue;
1931+
try {
1932+
for (size_t i = 0; i < count; i++) {
1933+
if (map_nbytes[i] == 0) {
1934+
continue;
1935+
}
1936+
if (webgpu_wait(instance_, map_futures[i]) != WGPUWaitStatus_Success) {
1937+
throw std::runtime_error("WebGPU: WaitAny failed for output map");
1938+
}
1939+
if (cb_data[i].status == WGPUMapAsyncStatus_Success) {
1940+
is_mapped[i] = true;
1941+
}
19331942
}
1934-
if (cb_data[i].status == WGPUMapAsyncStatus_Success) {
1943+
1944+
for (size_t i = 0; i < count; i++) {
1945+
if (map_nbytes[i] == 0) {
1946+
continue;
1947+
}
1948+
if (cb_data[i].status != WGPUMapAsyncStatus_Success) {
1949+
throw std::runtime_error("WebGPU buffer map failed for output");
1950+
}
19351951
const void* mapped = wgpuBufferGetConstMappedRange(
1936-
output_staging_buffers_[i], 0, outputs[i].second);
1937-
std::memcpy(outputs[i].first, mapped, outputs[i].second);
1952+
output_staging_buffers_[i], 0, map_nbytes[i]);
1953+
const size_t dst_nbytes = outputs[i].second;
1954+
if (dst_nbytes == map_nbytes[i]) {
1955+
std::memcpy(outputs[i].first, mapped, map_nbytes[i]);
1956+
} else if (
1957+
dst_nbytes == 2 * map_nbytes[i] && tensors_[output_ids_[i]].is_int &&
1958+
tensors_[output_ids_[i]].elem_size == 4) {
1959+
// int64 host output backed by an int32 GPU buffer: widen (sign-extend).
1960+
const int32_t* src = static_cast<const int32_t*>(mapped);
1961+
int64_t* dst = static_cast<int64_t*>(outputs[i].first);
1962+
const size_t n = map_nbytes[i] / sizeof(int32_t);
1963+
for (size_t k = 0; k < n; k++) {
1964+
dst[k] = static_cast<int64_t>(src[k]);
1965+
}
1966+
} else {
1967+
throw std::runtime_error("WebGPU: output buffer size mismatch");
1968+
}
19381969
wgpuBufferUnmap(output_staging_buffers_[i]);
1939-
} else {
1940-
throw std::runtime_error("WebGPU buffer map failed for output");
1970+
is_mapped[i] = false;
1971+
}
1972+
} catch (...) {
1973+
for (size_t j = 0; j < count; j++) {
1974+
if (is_mapped[j]) {
1975+
wgpuBufferUnmap(output_staging_buffers_[j]);
1976+
}
19411977
}
1978+
throw;
19421979
}
19431980
}
19441981

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ struct WebGPUTensor {
3434
// Serialized (GPU-side) element type, used to narrow wider host inputs.
3535
size_t elem_size = 0;
3636
bool is_int = false;
37+
// Exactly int8 (not uint8/bool), so int8-only ops can guard their dtype.
38+
bool is_int8 = false;
3739
};
3840

3941
// Host-side view of one graph input, passed to copy_inputs.

backends/webgpu/runtime/WebGPUUtils.h

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,63 @@ inline ComputePipelineBundle make_compute_pipeline(
457457
return bundle;
458458
}
459459

460+
// Builds another pipeline + bind group from resources owned by an earlier
461+
// bundle. The binding indices and types must match the shared bind-group
462+
// layout. This preserves shared-shader/layout multi-pipeline construction
463+
// without transferring ownership of those resources to the returned bundle.
464+
inline ComputePipelineBundle make_compute_pipeline(
465+
WGPUDevice device,
466+
const ComputePipelineBundle& shared_resources,
467+
const std::vector<BindingSpec>& bindings,
468+
const WGPUConstantEntry* constants = nullptr,
469+
size_t constant_count = 0,
470+
const char* entry_point = "main") {
471+
if (shared_resources.shader == nullptr ||
472+
shared_resources.bind_group_layout == nullptr ||
473+
shared_resources.pipeline_layout == nullptr) {
474+
throw std::runtime_error(
475+
"make_compute_pipeline: shared resources are not available");
476+
}
477+
478+
ComputePipelineBundle bundle;
479+
480+
std::vector<WGPUBindGroupEntry> bind_entries(bindings.size());
481+
for (size_t i = 0; i < bindings.size(); i++) {
482+
bind_entries[i] = {};
483+
bind_entries[i].binding = bindings[i].binding;
484+
bind_entries[i].buffer = bindings[i].buffer;
485+
bind_entries[i].size = bindings[i].size;
486+
}
487+
488+
WGPUComputePipelineDescriptor pipeline_desc = {};
489+
pipeline_desc.layout = shared_resources.pipeline_layout;
490+
pipeline_desc.compute.module = shared_resources.shader;
491+
pipeline_desc.compute.entryPoint = {entry_point, WGPU_STRLEN};
492+
pipeline_desc.compute.constantCount = constant_count;
493+
pipeline_desc.compute.constants = constants;
494+
WGPUComputePipeline pipeline =
495+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
496+
if (pipeline == nullptr) {
497+
throw std::runtime_error(
498+
"make_compute_pipeline: compute pipeline creation failed");
499+
}
500+
501+
WGPUBindGroupDescriptor bg_desc = {};
502+
bg_desc.layout = shared_resources.bind_group_layout;
503+
bg_desc.entryCount = bind_entries.size();
504+
bg_desc.entries = bind_entries.data();
505+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
506+
if (bind_group == nullptr) {
507+
wgpuComputePipelineRelease(pipeline);
508+
throw std::runtime_error(
509+
"make_compute_pipeline: bind group creation failed");
510+
}
511+
512+
bundle.pipeline = pipeline;
513+
bundle.bind_group = bind_group;
514+
return bundle;
515+
}
516+
460517
// The {wg_size, stride_x} override-constant pair every 2D-spill dispatch
461518
// builds from its DispatchGrid; was hand-rolled identically at 7 call sites.
462519
inline std::array<WGPUConstantEntry, 2> make_grid_constants(

backends/webgpu/runtime/ops/TensorMeta.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616

1717
namespace executorch::backends::webgpu {
1818

19-
constexpr uint32_t kTensorMetaMaxNdim = 4;
19+
constexpr uint32_t kTensorMetaMaxNdim = 8;
2020

21-
// Per-tensor metadata UBO; mirrors Vulkan BufferMetadata (4-dim NCHW, std140).
21+
// Per-tensor metadata UBO; mirrors Vulkan BufferMetadata (8-dim NCHW, std140).
2222
struct TensorMeta {
2323
uint32_t ndim;
2424
uint32_t numel;
@@ -28,19 +28,19 @@ struct TensorMeta {
2828
};
2929

3030
static_assert(
31-
sizeof(TensorMeta) == 48,
32-
"TensorMeta std140 layout must be 48 bytes to match the WGSL uniform");
31+
sizeof(TensorMeta) == 80,
32+
"TensorMeta std140 layout must be 80 bytes to match the WGSL uniform");
3333
// Lock the std140 field offsets the WGSL uniform reads, not just total size.
3434
static_assert(offsetof(TensorMeta, ndim) == 0);
3535
static_assert(offsetof(TensorMeta, numel) == 4);
3636
static_assert(offsetof(TensorMeta, sizes) == 16);
37-
static_assert(offsetof(TensorMeta, strides) == 32);
37+
static_assert(offsetof(TensorMeta, strides) == 48);
3838

3939
// Fill TensorMeta from NCHW dims: contiguous strides, padded trailing slots.
4040
inline void fill_tensor_meta(const WebGPUTensor& t, TensorMeta* m) {
4141
const uint32_t ndim = static_cast<uint32_t>(t.dims.size());
4242
if (ndim > kTensorMetaMaxNdim) {
43-
throw std::runtime_error("TensorMeta: tensor rank exceeds 4 (MAX_NDIM)");
43+
throw std::runtime_error("TensorMeta: tensor rank exceeds 8 (MAX_NDIM)");
4444
}
4545
*m = {};
4646
for (uint32_t d = 0; d < kTensorMetaMaxNdim; d++) {
@@ -67,7 +67,7 @@ inline void fill_tensor_meta_broadcast(
6767
TensorMeta* m) {
6868
const uint32_t rank = static_cast<uint32_t>(t.dims.size());
6969
if (out_ndim > kTensorMetaMaxNdim) {
70-
throw std::runtime_error("TensorMeta: out_ndim exceeds 4 (MAX_NDIM)");
70+
throw std::runtime_error("TensorMeta: out_ndim exceeds 8 (MAX_NDIM)");
7171
}
7272
if (rank > out_ndim) {
7373
throw std::runtime_error("TensorMeta: operand rank exceeds out_ndim");

backends/webgpu/runtime/ops/adamw/AdamwStep.cpp

Lines changed: 16 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -97,78 +97,26 @@ void adamw_step_impl(WebGPUGraph& graph, const std::vector<int>& args) {
9797
utils::make_uniform(device, &params, sizeof(params));
9898
graph.add_uniform_buffer_bytes(sizeof(params));
9999

100-
WGPUShaderSourceWGSL wgsl_desc = {};
101-
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
102-
wgsl_desc.code = {kAdamwStepWGSL, WGPU_STRLEN};
103-
WGPUShaderModuleDescriptor shader_desc = {};
104-
shader_desc.nextInChain = &wgsl_desc.chain;
105-
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
106-
107-
WGPUBindGroupLayoutEntry entries[5] = {};
108-
for (uint32_t i = 0; i <= 2; i++) {
109-
entries[i].binding = i;
110-
entries[i].visibility = WGPUShaderStage_Compute;
111-
entries[i].buffer.type = WGPUBufferBindingType_Storage;
112-
}
113-
entries[3].binding = 3;
114-
entries[3].visibility = WGPUShaderStage_Compute;
115-
entries[3].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
116-
entries[4].binding = 4;
117-
entries[4].visibility = WGPUShaderStage_Compute;
118-
entries[4].buffer.type = WGPUBufferBindingType_Uniform;
119-
120-
WGPUBindGroupLayoutDescriptor bgl_desc = {};
121-
bgl_desc.entryCount = 5;
122-
bgl_desc.entries = entries;
123-
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
124-
125-
WGPUPipelineLayoutDescriptor pl_desc = {};
126-
pl_desc.bindGroupLayoutCount = 1;
127-
pl_desc.bindGroupLayouts = &bgl;
128-
WGPUPipelineLayout pipeline_layout =
129-
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
130-
131100
WGPUConstantEntry wg_size_constant = {};
132101
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
133102
wg_size_constant.value = static_cast<double>(wg_size);
134103

135-
WGPUComputePipelineDescriptor pipeline_desc = {};
136-
pipeline_desc.layout = pipeline_layout;
137-
pipeline_desc.compute.module = shader;
138-
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
139-
pipeline_desc.compute.constantCount = 1;
140-
pipeline_desc.compute.constants = &wg_size_constant;
141-
WGPUComputePipeline pipeline =
142-
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
143-
144-
WGPUBindGroupEntry bg_entries[5] = {};
145-
bg_entries[0].binding = 0;
146-
bg_entries[0].buffer = param.buffer;
147-
bg_entries[0].size = param.nbytes;
148-
bg_entries[1].binding = 1;
149-
bg_entries[1].buffer = m.buffer;
150-
bg_entries[1].size = m.nbytes;
151-
bg_entries[2].binding = 2;
152-
bg_entries[2].buffer = v.buffer;
153-
bg_entries[2].size = v.nbytes;
154-
bg_entries[3].binding = 3;
155-
bg_entries[3].buffer = grad.buffer;
156-
bg_entries[3].size = grad.nbytes;
157-
bg_entries[4].binding = 4;
158-
bg_entries[4].buffer = uniform_buffer;
159-
bg_entries[4].size = sizeof(params);
160-
161-
WGPUBindGroupDescriptor bg_desc = {};
162-
bg_desc.layout = bgl;
163-
bg_desc.entryCount = 5;
164-
bg_desc.entries = bg_entries;
165-
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
166-
167-
graph.add_dispatch({pipeline, bind_group, workgroup_count, "adamw_step"});
168-
169-
wgpuShaderModuleRelease(shader);
170-
wgpuBindGroupLayoutRelease(bgl);
171-
wgpuPipelineLayoutRelease(pipeline_layout);
104+
utils::ComputePipelineBundle bundle = utils::make_compute_pipeline(
105+
device,
106+
kAdamwStepWGSL,
107+
{
108+
{0, WGPUBufferBindingType_Storage, param.buffer, param.nbytes},
109+
{1, WGPUBufferBindingType_Storage, m.buffer, m.nbytes},
110+
{2, WGPUBufferBindingType_Storage, v.buffer, v.nbytes},
111+
{3, WGPUBufferBindingType_ReadOnlyStorage, grad.buffer, grad.nbytes},
112+
{4, WGPUBufferBindingType_Uniform, uniform_buffer, sizeof(params)},
113+
},
114+
&wg_size_constant,
115+
1);
116+
117+
graph.add_dispatch(
118+
{bundle.pipeline, bundle.bind_group, workgroup_count, "adamw_step"});
119+
172120
graph.own_uniform_buffer(uniform_buffer);
173121
}
174122

0 commit comments

Comments
 (0)