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
6780WebGPUGraph::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
148168void 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+
161219void 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
173255WebGPUGraph::~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