Skip to content

Commit 005ae03

Browse files
pytorchbotJCNTH
andauthored
[ExecuTorch][WebGPU] SymInt arithmetic ops (add/sub/mul/floordiv) for dynamic shapes (#20712)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #20573 by @JulianCloudNTH ^ Please use this as the source of truth for the PR details, comments, and reviews ghstack PR base: https://github.com/pytorch/executorch/tree/gh/JulianCloudNTH/65/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/JulianCloudNTH/65/head Merge bot PR base: https://github.com/pytorch/executorch/tree/main Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/JulianCloudNTH/65/orig @diff-train-skip-merge --------- Co-authored-by: Julian Ng-Thow-Hing <juliannth@meta.com>
1 parent 9b7fd14 commit 005ae03

42 files changed

Lines changed: 3169 additions & 1369 deletions

Some content is hidden

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

backends/webgpu/CMakeLists.txt

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,9 @@ function(add_webgpu_native_test test_name test_src)
151151
endfunction()
152152

153153
if(EXECUTORCH_BUILD_WEBGPU_TEST)
154-
add_webgpu_native_test(webgpu_native_test test/test_webgpu_native.cpp)
155-
add_webgpu_native_test(
156-
webgpu_dispatch_order_test test/native/test_dispatch_order.cpp
157-
)
158-
add_webgpu_native_test(
159-
webgpu_scratch_buffer_test test/native/test_scratch_buffer.cpp
160-
)
161-
add_webgpu_native_test(
162-
webgpu_update_cache_test test/native/test_update_cache.cpp
163-
)
164-
165-
# Manifest-driven op-test framework: a generic gtest driver (webgpu_op_test) +
166-
# its device-free util unit test. GTest needs -DEXECUTORCH_BUILD_TESTS=ON.
154+
# All WebGPU native tests use GTest (device-dependent ones bring up the device
155+
# in their own main(); the fold unit test is device-free via gtest_main).
156+
# GTest needs -DEXECUTORCH_BUILD_TESTS=ON.
167157
if(NOT TARGET GTest::gtest)
168158
find_package(GTest QUIET)
169159
endif()
@@ -194,6 +184,36 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
194184
)
195185
target_compile_options(webgpu_op_test_util_test PRIVATE -fexceptions)
196186
set_property(TARGET webgpu_op_test_util_test PROPERTY CXX_STANDARD 17)
187+
188+
# Device-dependent native tests: each has its own main() that brings up the
189+
# device once, then RUN_ALL_TESTS(); link GTest::gtest (not gtest_main).
190+
add_webgpu_native_test(webgpu_native_test test/test_webgpu_native.cpp)
191+
target_link_libraries(webgpu_native_test PRIVATE GTest::gtest)
192+
add_webgpu_native_test(
193+
webgpu_dispatch_order_test test/native/test_dispatch_order.cpp
194+
)
195+
target_link_libraries(webgpu_dispatch_order_test PRIVATE GTest::gtest)
196+
add_webgpu_native_test(
197+
webgpu_scratch_buffer_test test/native/test_scratch_buffer.cpp
198+
)
199+
target_link_libraries(webgpu_scratch_buffer_test PRIVATE GTest::gtest)
200+
add_webgpu_native_test(
201+
webgpu_update_cache_test test/native/test_update_cache.cpp
202+
)
203+
target_link_libraries(webgpu_update_cache_test PRIVATE GTest::gtest)
204+
add_webgpu_native_test(
205+
webgpu_dynamic_shape_test test/native/test_dynamic_shape.cpp
206+
)
207+
target_link_libraries(webgpu_dynamic_shape_test PRIVATE GTest::gtest)
208+
add_webgpu_native_test(webgpu_index_test test/native/test_index.cpp)
209+
target_link_libraries(webgpu_index_test PRIVATE GTest::gtest)
210+
211+
# Device-free fold unit test (gtest_main provides main; no device needed).
212+
add_webgpu_native_test(
213+
webgpu_dispatch_2d_test test/native/test_dispatch_2d.cpp
214+
)
215+
target_link_libraries(
216+
webgpu_dispatch_2d_test PRIVATE GTest::gtest GTest::gtest_main
217+
)
197218
endif()
198-
add_webgpu_native_test(webgpu_index_test test/native/test_index.cpp)
199219
endif()

backends/webgpu/runtime/WebGPUBackend.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414

1515
#include <executorch/runtime/backend/interface.h>
1616
#include <executorch/runtime/core/error.h>
17+
#include <executorch/runtime/core/exec_aten/util/tensor_util.h>
1718
#include <executorch/runtime/platform/log.h>
1819

20+
#include <vector>
21+
1922
#include <new>
2023

2124
namespace executorch {
@@ -35,6 +38,7 @@ using executorch::runtime::Error;
3538
using executorch::runtime::EValue;
3639
using executorch::runtime::FreeableBuffer;
3740
using executorch::runtime::register_backend;
41+
using executorch::runtime::resize_tensor;
3842
using executorch::runtime::Result;
3943
using executorch::runtime::Span;
4044

@@ -100,19 +104,39 @@ Error WebGPUBackend::execute(
100104
// Copy inputs from EValue tensors to GPU buffers
101105
std::vector<InputData> inputs;
102106
inputs.reserve(num_inputs);
103-
for (size_t i = 0; i < num_inputs; i++) {
104-
const auto& tensor = args[i]->toTensor();
105-
const bool host_is_int64 =
106-
tensor.scalar_type() == executorch::aten::ScalarType::Long;
107-
inputs.push_back({tensor.const_data_ptr(), tensor.nbytes(), host_is_int64});
108-
}
109107
// Fail loud as a runtime Error so a throw never crosses the backend boundary.
110108
try {
109+
// Build the input list and, for dynamic shapes, shrink each input to its
110+
// live sizes before upload (mirrors Vulkan maybe_resize_input). No-op when
111+
// unchanged, so a static graph is byte-identical.
112+
for (size_t i = 0; i < num_inputs; i++) {
113+
const auto& tensor = args[i]->toTensor();
114+
const bool host_is_int64 =
115+
tensor.scalar_type() == executorch::aten::ScalarType::Long;
116+
inputs.push_back(
117+
{tensor.const_data_ptr(), tensor.nbytes(), host_is_int64});
118+
const auto sizes = tensor.sizes();
119+
std::vector<int64_t> new_dims(sizes.begin(), sizes.end());
120+
graph->resize_input(graph->input_ids()[i], new_dims);
121+
}
111122
graph->copy_inputs(inputs);
112123
graph->update_symints_from_inputs(inputs);
113124
graph->propagate_resize();
125+
// Resize each output EValue to its live shape so the readback length is
126+
// correct (mirrors Vulkan maybe_resize_output).
127+
for (size_t i = 0; i < num_outputs; i++) {
128+
const auto& cd = graph->cur_dims(graph->output_ids()[i]);
129+
std::vector<executorch::aten::SizesType> osizes(cd.begin(), cd.end());
130+
Error e = resize_tensor(
131+
args[num_inputs + i]->toTensor(),
132+
ArrayRef<executorch::aten::SizesType>(osizes.data(), osizes.size()));
133+
if (e != Error::Ok) {
134+
ET_LOG(Error, "WebGPU: output %zu resize failed", i);
135+
return Error::Internal;
136+
}
137+
}
114138
} catch (const std::exception& e) {
115-
ET_LOG(Error, "WebGPU input copy / symint refresh failed: %s", e.what());
139+
ET_LOG(Error, "WebGPU input/output resize / copy failed: %s", e.what());
116140
return Error::Internal;
117141
}
118142

backends/webgpu/runtime/WebGPUDevice.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ WebGPUContext create_webgpu_context() {
9090

9191
// TimedWaitAny lets webgpu_wait() block on futures via wgpuInstanceWaitAny.
9292
WGPUInstanceDescriptor instance_desc = {};
93-
#if defined(__EMSCRIPTEN__)
93+
// Vendored (buck) Dawn uses the older capabilities.* API; the rig's native
94+
// Dawn and emscripten's emdawnwebgpu (emcc 4.0.19+) use requiredFeatures.
95+
#if defined(WEBGPU_DAWN_INSTANCE_CAPABILITIES)
9496
instance_desc.capabilities.timedWaitAnyEnable = true;
9597
instance_desc.capabilities.timedWaitAnyMaxCount = 1;
9698
#else

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <executorch/backends/webgpu/runtime/WebGPUCompat.h>
1616
#include <executorch/backends/webgpu/runtime/WebGPUDevice.h>
1717

18+
#include <algorithm>
1819
#include <cstdlib>
1920
#include <cstring>
2021
#include <stdexcept>
@@ -62,6 +63,18 @@ bool vk_datatype_is_int(vkgraph::VkDataType dtype) {
6263
}
6364
}
6465

66+
// Normalize a possibly-negative dim against rank; throws (fail-loud) if OOR.
67+
int normalize_dim(int dim, int rank, const char* op) {
68+
if (dim < 0) {
69+
dim += rank;
70+
}
71+
if (dim < 0 || dim >= rank) {
72+
throw std::runtime_error(
73+
std::string("WebGPU ") + op + ": dim out of range");
74+
}
75+
return dim;
76+
}
77+
6578
} // namespace
6679

6780
WebGPUGraph::WebGPUGraph() = default;
@@ -104,11 +117,10 @@ void WebGPUGraph::update_symints_from_inputs(
104117
throw std::runtime_error(
105118
"select_as_symint: source tensor is not a graph input");
106119
}
107-
const auto& dims = tensors_[src.input_tensor_id].dims;
108-
int dim = src.dim < 0 ? src.dim + static_cast<int>(dims.size()) : src.dim;
109-
if (dim < 0 || dim >= static_cast<int>(dims.size())) {
110-
throw std::runtime_error("select_as_symint: dim out of range");
111-
}
120+
// Live cur_dims: the source may be a dynamic-shape input.
121+
const auto& dims = tensors_[src.input_tensor_id].cur_dims;
122+
int dim = normalize_dim(
123+
src.dim, static_cast<int>(dims.size()), "select_as_symint");
112124
int index = src.index;
113125
if (index < 0) {
114126
index += static_cast<int>(dims[dim]);
@@ -129,20 +141,26 @@ void WebGPUGraph::update_symints_from_inputs(
129141
}
130142
// Reads the [0,..,index,..,0] element; symint sources are scalar-ish.
131143
const int64_t offset = static_cast<int64_t>(index) * stride;
132-
// elem_size back-derived from build-time numel (sources are static-shaped).
133144
const void* host = inputs[pos].data;
134-
const size_t elem_size = inputs[pos].nbytes / static_cast<size_t>(numel);
145+
// Interpret the HOST buffer by its scalar type, not the tensor's serialized
146+
// elem_size: copy_inputs narrows an int64 host input to an int32 buffer, so
147+
// elem_size (buffer-derived) would misread int64 host data as int32.
135148
int32_t val;
136-
if (elem_size == sizeof(int64_t)) {
149+
if (inputs[pos].host_is_int64) {
137150
val = static_cast<int32_t>(static_cast<const int64_t*>(host)[offset]);
138-
} else if (elem_size == sizeof(int32_t)) {
139-
val = static_cast<const int32_t*>(host)[offset];
140151
} else {
141-
throw std::runtime_error(
142-
"select_as_symint: unsupported input element size");
152+
val = static_cast<const int32_t*>(host)[offset];
143153
}
144154
set_symint(src.symint_id, val);
145155
}
156+
// sym_size.int: SymInt = a tensor's live dim (cur_dims). Usually unused (ops
157+
// read cur_dims directly); for an intermediate source cur_dims is the build
158+
// max here (hooks run later in propagate_resize), which is fine while unused.
159+
for (const auto& s : symint_dim_sources_) {
160+
const auto& d = tensors_[s.tensor_id].cur_dims;
161+
int dim = normalize_dim(s.dim, static_cast<int>(d.size()), "sym_size");
162+
set_symint(s.symint_id, static_cast<int32_t>(d[dim]));
163+
}
146164
}
147165

148166
void WebGPUGraph::set_symint(int id, int32_t val) {
@@ -158,16 +176,78 @@ void WebGPUGraph::set_symint(int id, int32_t val) {
158176
}
159177
}
160178

179+
void WebGPUGraph::set_cur_dims(
180+
int value_id,
181+
const std::vector<int64_t>& new_dims) {
182+
auto& t = tensors_[value_id];
183+
if (new_dims.size() != t.dims.size()) {
184+
throw std::runtime_error("WebGPU resize: tensor rank changed");
185+
}
186+
size_t numel = 1;
187+
for (size_t d = 0; d < new_dims.size(); d++) {
188+
// 0-sized dims unsupported: live shapes are always in [1, max] per dim.
189+
if (new_dims[d] <= 0) {
190+
throw std::runtime_error("WebGPU resize: new dim must be positive");
191+
}
192+
if (new_dims[d] > t.dims[d]) {
193+
throw std::runtime_error(
194+
"WebGPU resize: new dim exceeds the max (serialized) allocation");
195+
}
196+
numel *= static_cast<size_t>(new_dims[d]);
197+
}
198+
const size_t new_nbytes = numel * t.elem_size;
199+
if (t.cur_dims != new_dims) {
200+
t.cur_dims = new_dims;
201+
t.cur_nbytes = new_nbytes;
202+
dirty_tensors_.insert(value_id);
203+
}
204+
}
205+
206+
void WebGPUGraph::resize_input(
207+
int value_id,
208+
const std::vector<int64_t>& new_dims) {
209+
if (std::find(input_ids_.begin(), input_ids_.end(), value_id) ==
210+
input_ids_.end()) {
211+
throw std::runtime_error(
212+
"WebGPUGraph::resize_input: value_id is not a graph input");
213+
}
214+
set_cur_dims(value_id, new_dims);
215+
}
216+
161217
void WebGPUGraph::propagate_resize() {
162-
if (dirty_symints_.empty()) {
218+
if (dirty_symints_.empty() && dirty_tensors_.empty()) {
163219
return;
164220
}
221+
// Hooks fire in registration (topological) order: operands update first.
165222
for (auto& hook : resize_hooks_) {
166223
if (dirty_symints_.count(hook.symint_id) != 0) {
167224
hook.fn(*this);
168225
}
169226
}
170227
dirty_symints_.clear();
228+
// Tensor hooks: bounded fixpoint. A hook may dirty its output (cascading to a
229+
// consumer); each pass handles the currently-dirty set. A forward DAG
230+
// converges in <= depth passes (set_cur_dims re-dirties only on a change).
231+
for (size_t pass = 0;
232+
!dirty_tensors_.empty() && pass <= tensor_resize_hooks_.size();
233+
pass++) {
234+
std::unordered_set<int> processing;
235+
processing.swap(dirty_tensors_);
236+
for (auto& hook : tensor_resize_hooks_) {
237+
if (processing.count(hook.trigger_tensor_id) != 0) {
238+
hook.fn(*this);
239+
}
240+
}
241+
}
242+
if (!dirty_tensors_.empty()) {
243+
throw std::runtime_error(
244+
"WebGPU resize: tensor resize hooks did not converge");
245+
}
246+
// Tensor hooks must not set_symint (dirty_symints_ already drained above).
247+
if (!dirty_symints_.empty()) {
248+
throw std::runtime_error(
249+
"WebGPU resize: a tensor resize hook set a SymInt; not supported");
250+
}
171251
}
172252

173253
WebGPUGraph::~WebGPUGraph() {
@@ -322,6 +402,10 @@ void WebGPUGraph::build(
322402
tensor.elem_size = vk_datatype_size(vk_tensor->datatype());
323403
tensor.is_int = vk_datatype_is_int(vk_tensor->datatype());
324404
tensor.nbytes = numel * tensor.elem_size;
405+
// Live dims start == max (serialized upper bound); resize_input shrinks
406+
// them per call. Static graphs keep cur == max forever.
407+
tensor.cur_dims = tensor.dims;
408+
tensor.cur_nbytes = tensor.nbytes;
325409

326410
int constant_id = vk_tensor->constant_id();
327411
int mem_obj_id = vk_tensor->mem_obj_id();
@@ -624,17 +708,20 @@ void WebGPUGraph::copy_inputs(const std::vector<InputData>& inputs) {
624708
}
625709
int tid = input_ids_[i];
626710
const auto& tensor = tensors_[tid];
711+
// Upload only the live (cur) bytes, not the max allocation; cur_nbytes ==
712+
// nbytes on a static graph, so this is byte-identical there.
713+
const size_t live_nbytes = tensor.cur_nbytes;
627714

628715
// Fast path: host and GPU element types match byte-for-byte.
629-
if (in.nbytes == tensor.nbytes) {
630-
wgpuQueueWriteBuffer(queue_, tensor.buffer, 0, in.data, tensor.nbytes);
716+
if (in.nbytes == live_nbytes) {
717+
wgpuQueueWriteBuffer(queue_, tensor.buffer, 0, in.data, live_nbytes);
631718
continue;
632719
}
633720

634721
// Narrow int64 host indices into the int32 buffer (mirrors Vulkan).
635722
const bool buffer_is_int32 = tensor.is_int && tensor.elem_size == 4;
636-
if (in.host_is_int64 && buffer_is_int32 && in.nbytes == tensor.nbytes * 2) {
637-
const size_t numel = tensor.nbytes / 4;
723+
if (in.host_is_int64 && buffer_is_int32 && in.nbytes == live_nbytes * 2) {
724+
const size_t numel = live_nbytes / 4;
638725
const int64_t* src = static_cast<const int64_t*>(in.data);
639726
std::vector<int32_t> narrowed(numel);
640727
for (size_t e = 0; e < numel; e++) {
@@ -648,15 +735,15 @@ void WebGPUGraph::copy_inputs(const std::vector<InputData>& inputs) {
648735
narrowed[e] = static_cast<int32_t>(src[e]);
649736
}
650737
wgpuQueueWriteBuffer(
651-
queue_, tensor.buffer, 0, narrowed.data(), tensor.nbytes);
738+
queue_, tensor.buffer, 0, narrowed.data(), live_nbytes);
652739
continue;
653740
}
654741

655742
throw std::runtime_error(
656743
"WebGPU: unsupported input copy for input " + std::to_string(i) +
657744
" (host " + std::to_string(in.nbytes) + " bytes" +
658745
(in.host_is_int64 ? " int64" : "") + " vs buffer " +
659-
std::to_string(tensor.nbytes) + " bytes)");
746+
std::to_string(live_nbytes) + " bytes)");
660747
}
661748
}
662749

@@ -727,15 +814,15 @@ void WebGPUGraph::execute() {
727814
wgpuComputePassEncoderSetBindGroup(
728815
pass, 0, dispatch.bind_group, 0, nullptr);
729816
wgpuComputePassEncoderDispatchWorkgroups(
730-
pass, dispatch.workgroup_count_x, 1, 1);
817+
pass, dispatch.workgroup_count_x, dispatch.workgroup_count_y, 1);
731818
wgpuComputePassEncoderEnd(pass);
732819
wgpuComputePassEncoderRelease(pass);
733820
#ifdef WGPU_BACKEND_ENABLE_PROFILING
734821
if (qp) {
735822
qp->record(
736823
static_cast<uint32_t>(i),
737824
dispatch.kernel_name,
738-
{dispatch.workgroup_count_x, 1, 1},
825+
{dispatch.workgroup_count_x, dispatch.workgroup_count_y, 1},
739826
{1, 1, 1});
740827
}
741828
#endif // WGPU_BACKEND_ENABLE_PROFILING
@@ -807,7 +894,10 @@ void WebGPUGraph::execute() {
807894
wgpuComputePassEncoderSetBindGroup(
808895
pass, 0, dispatches_[i].bind_group, 0, nullptr);
809896
wgpuComputePassEncoderDispatchWorkgroups(
810-
pass, dispatches_[i].workgroup_count_x, 1, 1);
897+
pass,
898+
dispatches_[i].workgroup_count_x,
899+
dispatches_[i].workgroup_count_y,
900+
1);
811901
wgpuComputePassEncoderEnd(pass);
812902
wgpuComputePassEncoderRelease(pass);
813903
}

0 commit comments

Comments
 (0)