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,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
148166void 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+
161217void 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
173253WebGPUGraph::~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