Skip to content

Commit 7e67e98

Browse files
committed
Update (base update)
[ghstack-poisoned]
1 parent 73c259e commit 7e67e98

31 files changed

Lines changed: 2214 additions & 234 deletions

backends/webgpu/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
161161
add_webgpu_native_test(
162162
webgpu_update_cache_test test/native/test_update_cache.cpp
163163
)
164+
add_webgpu_native_test(
165+
webgpu_dynamic_shape_test test/native/test_dynamic_shape.cpp
166+
)
167+
add_webgpu_native_test(
168+
webgpu_dispatch_2d_test test/native/test_dispatch_2d.cpp
169+
)
164170

165171
# Manifest-driven op-test framework: a generic gtest driver (webgpu_op_test) +
166172
# its device-free util unit test. GTest needs -DEXECUTORCH_BUILD_TESTS=ON.

backends/webgpu/runtime/WebGPUBackend.cpp

Lines changed: 26 additions & 1 deletion
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

@@ -108,11 +112,32 @@ Error WebGPUBackend::execute(
108112
}
109113
// Fail loud as a runtime Error so a throw never crosses the backend boundary.
110114
try {
115+
// Dynamic shapes: shrink each input to its live sizes before upload
116+
// (mirrors Vulkan maybe_resize_input). No-op when unchanged, so a static
117+
// graph is byte-identical.
118+
for (size_t i = 0; i < num_inputs; i++) {
119+
const auto sizes = args[i]->toTensor().sizes();
120+
std::vector<int64_t> new_dims(sizes.begin(), sizes.end());
121+
graph->resize_input(graph->input_ids()[i], new_dims);
122+
}
111123
graph->copy_inputs(inputs);
112124
graph->update_symints_from_inputs(inputs);
113125
graph->propagate_resize();
126+
// Resize each output EValue to its live shape so the readback length is
127+
// correct (mirrors Vulkan maybe_resize_output).
128+
for (size_t i = 0; i < num_outputs; i++) {
129+
const auto& cd = graph->cur_dims(graph->output_ids()[i]);
130+
std::vector<executorch::aten::SizesType> osizes(cd.begin(), cd.end());
131+
Error e = resize_tensor(
132+
args[num_inputs + i]->toTensor(),
133+
ArrayRef<executorch::aten::SizesType>(osizes.data(), osizes.size()));
134+
if (e != Error::Ok) {
135+
ET_LOG(Error, "WebGPU: output %zu resize failed", i);
136+
return Error::Internal;
137+
}
138+
}
114139
} catch (const std::exception& e) {
115-
ET_LOG(Error, "WebGPU input copy / symint refresh failed: %s", e.what());
140+
ET_LOG(Error, "WebGPU input/output resize / copy failed: %s", e.what());
116141
return Error::Internal;
117142
}
118143

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 109 additions & 17 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,9 +141,9 @@ 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+
// Stored elem_size (live nbytes/numel mis-derives for a dynamic source).
146+
const size_t elem_size = tensors_[src.input_tensor_id].elem_size;
135147
int32_t val;
136148
if (elem_size == sizeof(int64_t)) {
137149
val = static_cast<int32_t>(static_cast<const int64_t*>(host)[offset]);
@@ -143,6 +155,14 @@ void WebGPUGraph::update_symints_from_inputs(
143155
}
144156
set_symint(src.symint_id, val);
145157
}
158+
// sym_size.int: SymInt = a tensor's live dim (cur_dims). Usually unused (ops
159+
// read cur_dims directly); for an intermediate source cur_dims is the build
160+
// max here (hooks run later in propagate_resize), which is fine while unused.
161+
for (const auto& s : symint_dim_sources_) {
162+
const auto& d = tensors_[s.tensor_id].cur_dims;
163+
int dim = normalize_dim(s.dim, static_cast<int>(d.size()), "sym_size");
164+
set_symint(s.symint_id, static_cast<int32_t>(d[dim]));
165+
}
146166
}
147167

148168
void WebGPUGraph::set_symint(int id, int32_t val) {
@@ -158,16 +178,78 @@ void WebGPUGraph::set_symint(int id, int32_t val) {
158178
}
159179
}
160180

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

173255
WebGPUGraph::~WebGPUGraph() {
@@ -322,6 +404,10 @@ void WebGPUGraph::build(
322404
tensor.elem_size = vk_datatype_size(vk_tensor->datatype());
323405
tensor.is_int = vk_datatype_is_int(vk_tensor->datatype());
324406
tensor.nbytes = numel * tensor.elem_size;
407+
// Live dims start == max (serialized upper bound); resize_input shrinks
408+
// them per call. Static graphs keep cur == max forever.
409+
tensor.cur_dims = tensor.dims;
410+
tensor.cur_nbytes = tensor.nbytes;
325411

326412
int constant_id = vk_tensor->constant_id();
327413
int mem_obj_id = vk_tensor->mem_obj_id();
@@ -624,17 +710,20 @@ void WebGPUGraph::copy_inputs(const std::vector<InputData>& inputs) {
624710
}
625711
int tid = input_ids_[i];
626712
const auto& tensor = tensors_[tid];
713+
// Upload only the live (cur) bytes, not the max allocation; cur_nbytes ==
714+
// nbytes on a static graph, so this is byte-identical there.
715+
const size_t live_nbytes = tensor.cur_nbytes;
627716

628717
// 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);
718+
if (in.nbytes == live_nbytes) {
719+
wgpuQueueWriteBuffer(queue_, tensor.buffer, 0, in.data, live_nbytes);
631720
continue;
632721
}
633722

634723
// Narrow int64 host indices into the int32 buffer (mirrors Vulkan).
635724
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;
725+
if (in.host_is_int64 && buffer_is_int32 && in.nbytes == live_nbytes * 2) {
726+
const size_t numel = live_nbytes / 4;
638727
const int64_t* src = static_cast<const int64_t*>(in.data);
639728
std::vector<int32_t> narrowed(numel);
640729
for (size_t e = 0; e < numel; e++) {
@@ -648,15 +737,15 @@ void WebGPUGraph::copy_inputs(const std::vector<InputData>& inputs) {
648737
narrowed[e] = static_cast<int32_t>(src[e]);
649738
}
650739
wgpuQueueWriteBuffer(
651-
queue_, tensor.buffer, 0, narrowed.data(), tensor.nbytes);
740+
queue_, tensor.buffer, 0, narrowed.data(), live_nbytes);
652741
continue;
653742
}
654743

655744
throw std::runtime_error(
656745
"WebGPU: unsupported input copy for input " + std::to_string(i) +
657746
" (host " + std::to_string(in.nbytes) + " bytes" +
658747
(in.host_is_int64 ? " int64" : "") + " vs buffer " +
659-
std::to_string(tensor.nbytes) + " bytes)");
748+
std::to_string(live_nbytes) + " bytes)");
660749
}
661750
}
662751

@@ -727,15 +816,15 @@ void WebGPUGraph::execute() {
727816
wgpuComputePassEncoderSetBindGroup(
728817
pass, 0, dispatch.bind_group, 0, nullptr);
729818
wgpuComputePassEncoderDispatchWorkgroups(
730-
pass, dispatch.workgroup_count_x, 1, 1);
819+
pass, dispatch.workgroup_count_x, dispatch.workgroup_count_y, 1);
731820
wgpuComputePassEncoderEnd(pass);
732821
wgpuComputePassEncoderRelease(pass);
733822
#ifdef WGPU_BACKEND_ENABLE_PROFILING
734823
if (qp) {
735824
qp->record(
736825
static_cast<uint32_t>(i),
737826
dispatch.kernel_name,
738-
{dispatch.workgroup_count_x, 1, 1},
827+
{dispatch.workgroup_count_x, dispatch.workgroup_count_y, 1},
739828
{1, 1, 1});
740829
}
741830
#endif // WGPU_BACKEND_ENABLE_PROFILING
@@ -807,7 +896,10 @@ void WebGPUGraph::execute() {
807896
wgpuComputePassEncoderSetBindGroup(
808897
pass, 0, dispatches_[i].bind_group, 0, nullptr);
809898
wgpuComputePassEncoderDispatchWorkgroups(
810-
pass, dispatches_[i].workgroup_count_x, 1, 1);
899+
pass,
900+
dispatches_[i].workgroup_count_x,
901+
dispatches_[i].workgroup_count_y,
902+
1);
811903
wgpuComputePassEncoderEnd(pass);
812904
wgpuComputePassEncoderRelease(pass);
813905
}

0 commit comments

Comments
 (0)