Skip to content

Commit 3fe5cbe

Browse files
authored
Run WebGPU models in browser Wasm (#23938) (#23938)
Summary: ### 🎯 Why The opt-in ExecuTorch Wasm build cannot run WebGPU-delegated models in a browser. This change links the WebGPU backend into that build and removes two op limits that large vision models hit. ### 🔧 What changed - Link the WebGPU backend into the opt-in ExecuTorch Wasm build behind EXECUTORCH_BUILD_WASM_WEBGPU (off by default). - Initialize one process-wide WebGPU context through a new initWebGPU() binding. - Enable Asyncify, exceptions, and a growable heap with a 4 GB maximum in that configuration only. - Pin Emscripten 4.0.19, the first pinned version whose emdawnwebgpu port has the instance-features API that the backend uses. - Support batched aten.linear inputs by flattening leading dimensions. - Fold large aten.where dispatches across two workgroup dimensions. - Build the WebGPU Wasm configuration in pull.yml (build only, because running a delegated model needs a browser with WebGPU). - Add configuration tests that check the CMake option, link flags, bindings, build script, and CI pin. A build without EXECUTORCH_BUILD_WASM_WEBGPU is unchanged. Reviewed By: psiddh Differential Revision: D117468920
1 parent 596a5a8 commit 3fe5cbe

14 files changed

Lines changed: 567 additions & 36 deletions

File tree

‎.ci/scripts/setup-emscripten.sh‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ install_node() {
1515
}
1616

1717
install_emscripten() {
18+
# The WebGPU backend's instance API (WGPUInstanceFeatureName /
19+
# WGPUInstanceDescriptor::requiredFeatures) needs the emdawnwebgpu port
20+
# pinned by Emscripten 4.0.13 or newer; 4.0.10 ships the older
21+
# capabilities-based descriptor and does not compile it.
22+
local emscripten_version="${EMSCRIPTEN_VERSION:-4.0.19}"
1823
git clone https://github.com/emscripten-core/emsdk.git
1924
pushd emsdk || return
20-
./emsdk install 4.0.10
21-
./emsdk activate 4.0.10
25+
./emsdk install "${emscripten_version}"
26+
./emsdk activate "${emscripten_version}"
2227
source ./emsdk_env.sh
2328
popd || return
2429
}

‎.github/workflows/pull.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,6 +1581,10 @@ jobs:
15811581
# Test selective build
15821582
PYTHON_EXECUTABLE=python bash examples/wasm/test_build_wasm.sh
15831583
1584+
# Build the WebGPU configuration of the Wasm bindings. Build only:
1585+
# running a delegated model needs a browser with WebGPU.
1586+
PYTHON_EXECUTABLE=python bash scripts/build_wasm_tests.sh --webgpu
1587+
15841588
unittest-wasm-bindings:
15851589
needs: docker-image
15861590
name: unittest-wasm-bindings

‎CMakeLists.txt‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,10 +1543,6 @@ if(EXECUTORCH_BUILD_PYBIND)
15431543
executorch_target_copy_mlx_metallib(portable_lib)
15441544
endif()
15451545

1546-
if(EXECUTORCH_BUILD_WASM)
1547-
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/wasm)
1548-
endif()
1549-
15501546
if(EXECUTORCH_BUILD_TOKENIZERS_WASM)
15511547
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/wasm/tokenizers)
15521548
endif()
@@ -1602,6 +1598,12 @@ if(EXECUTORCH_BUILD_VGF)
16021598
list(APPEND _executorch_backends vgf_backend)
16031599
endif()
16041600

1601+
# Added after the backends so EXECUTORCH_BUILD_WASM_WEBGPU can link
1602+
# webgpu_backend, which has to already be defined at that point.
1603+
if(EXECUTORCH_BUILD_WASM)
1604+
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/wasm)
1605+
endif()
1606+
16051607
# Top-level interface targets.
16061608

16071609
# A target containing all configured backends.

‎backends/webgpu/runtime/ops/linear/Linear.cpp‎

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

1717
#include <cstdint>
1818
#include <cstring>
19+
#include <limits>
1920
#include <stdexcept>
2021
#include <vector>
2122

@@ -53,21 +54,39 @@ void linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
5354
const auto& w = graph.get_tensor(w_id);
5455
const auto& out = graph.get_tensor(out_id);
5556

56-
if (in.dims.size() != 2 || w.dims.size() != 2) {
57-
throw std::runtime_error("WebGPU linear: input/weight must be 2D");
57+
if (in.dims.empty() || w.dims.size() != 2) {
58+
throw std::runtime_error(
59+
"WebGPU linear: input must have rank >= 1 and weight must be 2D");
5860
}
59-
const uint32_t M = static_cast<uint32_t>(in.dims[0]);
60-
const uint32_t K = static_cast<uint32_t>(in.dims[1]);
61-
const uint32_t N = static_cast<uint32_t>(w.dims[0]);
62-
if (static_cast<uint32_t>(w.dims[1]) != K) {
63-
throw std::runtime_error("WebGPU linear: weight.dims[1] != input.dims[1]");
61+
const int64_t k_dim = in.dims.back();
62+
const int64_t n_dim = w.dims[0];
63+
if (k_dim <= 0 || n_dim <= 0 || w.dims[1] != k_dim) {
64+
throw std::runtime_error(
65+
"WebGPU linear: invalid weight or input K dimension");
6466
}
65-
if (M == 0 || N == 0 || K == 0) {
66-
throw std::runtime_error("WebGPU linear: M, N, or K == 0");
67+
const uint64_t input_numel = utils::numel_of(in.dims);
68+
if (input_numel % static_cast<uint64_t>(k_dim) != 0u) {
69+
throw std::runtime_error(
70+
"WebGPU linear: input numel is not a multiple of K");
71+
}
72+
const uint64_t m_dim = input_numel / static_cast<uint64_t>(k_dim);
73+
if (m_dim == 0u || m_dim > std::numeric_limits<uint32_t>::max() ||
74+
static_cast<uint64_t>(n_dim) > std::numeric_limits<uint32_t>::max() ||
75+
static_cast<uint64_t>(k_dim) > std::numeric_limits<uint32_t>::max()) {
76+
throw std::runtime_error("WebGPU linear: M, N, or K exceeds uint32 range");
77+
}
78+
const uint32_t M = static_cast<uint32_t>(m_dim);
79+
const uint32_t K = static_cast<uint32_t>(k_dim);
80+
const uint32_t N = static_cast<uint32_t>(n_dim);
81+
82+
std::vector<int64_t> expected_out_dims = in.dims;
83+
expected_out_dims.back() = n_dim;
84+
if (out.dims != expected_out_dims) {
85+
throw std::runtime_error("WebGPU linear: output shape mismatch");
6786
}
6887

69-
const uint64_t outputs = static_cast<uint64_t>(M) * static_cast<uint64_t>(N);
70-
if (in.nbytes != static_cast<uint64_t>(M) * K * sizeof(float) ||
88+
const uint64_t outputs = static_cast<uint64_t>(M) * N;
89+
if (in.nbytes != input_numel * sizeof(float) ||
7190
w.nbytes != static_cast<uint64_t>(N) * K * sizeof(float) ||
7291
out.nbytes != outputs * sizeof(float)) {
7392
throw std::runtime_error("WebGPU linear: fp32-only (byte-size mismatch)");
@@ -142,6 +161,9 @@ void linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
142161
[in_id, out_id, M, N, K, has_bias, dispatch_x, dispatch_idx, params_buf](
143162
WebGPUGraph& g) {
144163
const auto& d = g.cur_dims(in_id);
164+
if (d.empty() || d.back() != static_cast<int64_t>(K)) {
165+
throw std::runtime_error("WebGPU linear: live K changed");
166+
}
145167
const uint64_t numel = utils::numel_of(d);
146168
if (numel % static_cast<uint64_t>(K) != 0u) {
147169
throw std::runtime_error(
@@ -158,8 +180,9 @@ void linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
158180
g.dispatch_at(dispatch_idx).workgroup_count_x = dispatch_x;
159181
g.dispatch_at(dispatch_idx).workgroup_count_y =
160182
(m + kTile - 1u) / kTile;
161-
g.set_cur_dims(
162-
out_id, {static_cast<int64_t>(m), static_cast<int64_t>(N)});
183+
std::vector<int64_t> output_dims = d;
184+
output_dims.back() = static_cast<int64_t>(N);
185+
g.set_cur_dims(out_id, output_dims);
163186
});
164187

165188
graph.own_uniform_buffer(uniform_buffer);

‎backends/webgpu/runtime/ops/where/Where.cpp‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ void where_impl(WebGPUGraph& graph, const std::vector<int>& args) {
7575
const size_t cond_bind_size = (cond_tensor.nbytes + 3) & ~size_t(3);
7676

7777
uint32_t wg_size = utils::clamp_workgroup_size(device, kWhereWorkgroupSizeX);
78-
uint32_t workgroup_count = utils::compute_1d_workgroup_count(
78+
utils::WgCount workgroup_count = utils::compute_2d_workgroup_count(
7979
device, out_meta.numel, wg_size, "where");
8080

8181
WGPUConstantEntry wg_size_constant = {};
@@ -120,8 +120,12 @@ void where_impl(WebGPUGraph& graph, const std::vector<int>& args) {
120120
&wg_size_constant,
121121
1);
122122

123-
const size_t dispatch_idx =
124-
graph.add_dispatch({bundle.pipeline, bundle.bind_group, workgroup_count});
123+
const size_t dispatch_idx = graph.add_dispatch(
124+
{bundle.pipeline,
125+
bundle.bind_group,
126+
workgroup_count.x,
127+
"where",
128+
workgroup_count.y});
125129

126130
// Dynamic shapes: rebuild the 4 broadcast TensorMeta UBOs + dispatch count.
127131
WGPUBuffer o_buf = out_meta_buf, c_buf = cond_meta_buf, a_buf = a_meta_buf,
@@ -170,9 +174,10 @@ void where_impl(WebGPUGraph& graph, const std::vector<int>& args) {
170174
wgpuQueueWriteBuffer(g.queue(), c_buf, 0, &cm, sizeof(cm));
171175
wgpuQueueWriteBuffer(g.queue(), a_buf, 0, &am, sizeof(am));
172176
wgpuQueueWriteBuffer(g.queue(), bb_buf, 0, &bm, sizeof(bm));
173-
g.dispatch_at(dispatch_idx).workgroup_count_x =
174-
utils::compute_1d_workgroup_count(
175-
g.device(), om.numel, wg_size, "where(resize)");
177+
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
178+
g.device(), om.numel, wg_size, "where(resize)");
179+
g.dispatch_at(dispatch_idx).workgroup_count_x = wgc.x;
180+
g.dispatch_at(dispatch_idx).workgroup_count_y = wgc.y;
176181
};
177182
graph.add_tensor_resize_hook(cond_id, where_resize);
178183
graph.add_tensor_resize_hook(a_id, where_resize);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ fn cond_is_true(i: u32) -> bool {
2323
}
2424

2525
@compute @workgroup_size(wg_size, 1, 1)
26-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
27-
let idx = gid.x;
26+
fn main(
27+
@builtin(global_invocation_id) gid: vec3<u32>,
28+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
29+
let idx = gid.x + gid.y * (num_workgroups.x * wg_size);
2830
if (idx >= out_meta.numel) {
2931
return;
3032
}

‎backends/webgpu/runtime/ops/where/where_wgsl.h‎

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

1515
// @generated from where.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: 2c5c6491e95822f767920ad82b81b005808ad5550567b85fa98c1521f188dbfc
16+
// wgsl-sha256: f254e27661bfaa523237e63f9ccd90665daa436e45f769903ee581b006b06576
1717
inline constexpr const char* kWhereWGSL = R"(
1818
@group(0) @binding(0) var<storage, read> cond: array<u32>;
1919
@group(0) @binding(1) var<storage, read> input_a: array<f32>;
@@ -40,8 +40,10 @@ fn cond_is_true(i: u32) -> bool {
4040
}
4141
4242
@compute @workgroup_size(wg_size, 1, 1)
43-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
44-
let idx = gid.x;
43+
fn main(
44+
@builtin(global_invocation_id) gid: vec3<u32>,
45+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
46+
let idx = gid.x + gid.y * (num_workgroups.x * wg_size);
4547
if (idx >= out_meta.numel) {
4648
return;
4749
}

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

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@
192192
CONFIGS as _VIEW_CONFIGS,
193193
ViewModule,
194194
)
195+
from executorch.backends.webgpu.test.ops.test_where import (
196+
_fp64_golden as _where_fp64_golden,
197+
WhereModule,
198+
)
195199

196200
# rms_norm coverage is exactly the 15 cases the native test covered.
197201
RMS_NORM_CASES = _CASES
@@ -1332,6 +1336,47 @@ def _layer_norm_suite() -> WebGPUTestSuite:
13321336
)
13331337

13341338

1339+
def _where_cond(shape):
1340+
return (torch.arange(torch.Size(shape).numel()).reshape(shape) % 2) == 0
1341+
1342+
1343+
def _where_a(shape):
1344+
return torch.linspace(-1.0, 1.0, torch.Size(shape).numel()).reshape(shape)
1345+
1346+
1347+
def _where_b(shape):
1348+
return torch.linspace(1.0, -1.0, torch.Size(shape).numel()).reshape(shape)
1349+
1350+
1351+
def _where_golden(_module, inputs):
1352+
return _where_fp64_golden(*inputs)
1353+
1354+
1355+
@register_op_test("where")
1356+
def _where_suite() -> WebGPUTestSuite:
1357+
def case(name, shape):
1358+
return Case(
1359+
name=name,
1360+
construct={},
1361+
inputs=(
1362+
InputSpec(shape=shape, gen=_where_cond),
1363+
InputSpec(shape=shape, gen=_where_a),
1364+
InputSpec(shape=shape, gen=_where_b),
1365+
),
1366+
golden_fn=_where_golden,
1367+
)
1368+
1369+
return WebGPUTestSuite(
1370+
module_factory=WhereModule,
1371+
cases=[
1372+
case("equal", (4, 8)),
1373+
case("large_2d_dispatch", (16, 576, 576)),
1374+
],
1375+
atol=1e-4,
1376+
rtol=1e-3,
1377+
)
1378+
1379+
13351380
from executorch.backends.webgpu.test.ops.test_linear_fp32 import (
13361381
_ramp as _lin_ramp,
13371382
make_linear,
@@ -1340,8 +1385,10 @@ def _layer_norm_suite() -> WebGPUTestSuite:
13401385

13411386
@register_op_test("linear_fp32")
13421387
def _linear_fp32_suite() -> WebGPUTestSuite:
1343-
# fp32 linear (BART + DaViT projections); bias + no-bias, and shapes whose
1344-
# M*N exceeds the 65535 1D ceiling to exercise the 2D-dispatch spill.
1388+
# fp32 linear (BART, DaViT, and vision-encoder projections); bias +
1389+
# no-bias, rank-2/3/4 inputs, and shapes whose M*N exceeds the 65535 1D
1390+
# ceiling to exercise the 2D-dispatch spill.
1391+
dynamic_batch = torch.export.Dim("linear_rank4_batch", min=1, max=2)
13451392
return WebGPUTestSuite(
13461393
module_factory=make_linear,
13471394
cases=[
@@ -1360,6 +1407,23 @@ def _linear_fp32_suite() -> WebGPUTestSuite:
13601407
construct={"in_features": 768, "out_features": 768},
13611408
inputs=(InputSpec(shape=(1, 16, 768), gen=_lin_ramp),),
13621409
),
1410+
Case(
1411+
name="rank4_bias",
1412+
construct={"in_features": 64, "out_features": 96},
1413+
inputs=(InputSpec(shape=(2, 3, 4, 64), gen=_lin_ramp),),
1414+
),
1415+
Case(
1416+
name="rank4_no_bias",
1417+
construct={"in_features": 64, "out_features": 96, "bias": False},
1418+
inputs=(InputSpec(shape=(2, 3, 4, 64), gen=_lin_ramp),),
1419+
),
1420+
Case(
1421+
name="dynamic_rank4_batch_2_to_1",
1422+
construct={"in_features": 64, "out_features": 96},
1423+
export_inputs=(InputSpec(shape=(2, 3, 4, 64), gen=_lin_ramp),),
1424+
inputs=(InputSpec(shape=(1, 3, 4, 64), gen=_lin_ramp),),
1425+
dynamic_shapes=({0: dynamic_batch},),
1426+
),
13631427
Case(
13641428
name="tall_m",
13651429
construct={"in_features": 128, "out_features": 64},

‎extension/wasm/CMakeLists.txt‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ list(
5050
extension_runner_util
5151
)
5252

53+
if(EXECUTORCH_BUILD_WASM_WEBGPU)
54+
if(NOT TARGET webgpu_backend)
55+
message(
56+
FATAL_ERROR
57+
"EXECUTORCH_BUILD_WASM_WEBGPU requires the webgpu_backend target. "
58+
"Enable EXECUTORCH_BUILD_WEBGPU and add backends/webgpu before "
59+
"extension/wasm."
60+
)
61+
endif()
62+
list(APPEND link_libraries webgpu_backend)
63+
endif()
64+
5365
add_library(executorch_wasm OBJECT wasm_bindings.cpp)
5466

5567
if(EXECUTORCH_ENABLE_EVENT_TRACER)
@@ -69,6 +81,26 @@ target_link_libraries(
6981
INTERFACE executorch_kernels
7082
)
7183

84+
if(EXECUTORCH_BUILD_WASM_WEBGPU)
85+
target_compile_definitions(executorch_wasm PUBLIC EXECUTORCH_WASM_WEBGPU)
86+
# Delegated execution blocks on wgpuInstanceWaitAny, which emdawnwebgpu
87+
# implements with Asyncify.handleAsync; the port aborts in that call when
88+
# Asyncify is off. ASYNCIFY_IGNORE_INDIRECT must stay unset: the wait is
89+
# reached through the virtual backend interface, an indirect call. Model data
90+
# lives in the Wasm heap before delegate upload, so permit growth beyond
91+
# Emscripten's 16 MiB default and retain the full wasm32 address-space
92+
# ceiling.
93+
target_link_options(
94+
executorch_wasm PUBLIC -sASYNCIFY -sASYNCIFY_STACK_SIZE=65536
95+
-sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=4GB
96+
)
97+
# The backend reports device and graph failures by throwing, and catches them
98+
# at its own boundary; Emscripten needs the flag when compiling and when
99+
# linking.
100+
target_compile_options(executorch_wasm PUBLIC -fexceptions)
101+
target_link_options(executorch_wasm PUBLIC -fexceptions)
102+
endif()
103+
72104
if(BUILD_TESTING)
73105
add_subdirectory(test)
74106
endif()

0 commit comments

Comments
 (0)