@@ -26,6 +26,10 @@ namespace executorch::backends::webgpu {
2626
2727namespace {
2828
29+ // Op name the AOT exporter emits for a prepacked constant (must match the
30+ // serialized schema); compared in the prepack pre-scan below.
31+ constexpr const char * kPrepackOpName = " et_vk.prepack.default" ;
32+
2933size_t vk_datatype_size (vkgraph::VkDataType dtype) {
3034 switch (dtype) {
3135 case vkgraph::VkDataType::BOOL :
@@ -230,6 +234,10 @@ void WebGPUGraph::build(
230234
231235 const auto * graph = vkgraph::GetVkGraph (flatbuffer_data);
232236
237+ // .pte byte sources for prepack-time constant materialization (build-only).
238+ constant_data_ = constant_data;
239+ named_data_map_ = named_data_map;
240+
233241 // Phase 1: Create all values
234242 const auto * values = graph->values ();
235243 const int num_vals = values ? values->size () : 0 ;
@@ -241,6 +249,41 @@ void WebGPUGraph::build(
241249 bools_.resize (num_vals, false );
242250 value_lists_.resize (num_vals);
243251
252+ // Pre-scan the op chain: a constant may be DEFERRED (no eager GPU buffer; the
253+ // prepack node materializes it once) only if it is a prepack source AND never
254+ // a direct arg of a non-prepack op. ValueList args are expanded so a constant
255+ // reached through a list still counts as a direct use.
256+ std::unordered_set<int > prepack_src_ids;
257+ std::unordered_set<int > direct_use_ids;
258+ const auto * chain_prescan = graph->chain ();
259+ if (chain_prescan) {
260+ for (unsigned ci = 0 ; ci < chain_prescan->size (); ci++) {
261+ const auto * oc = chain_prescan->Get (ci);
262+ const bool is_prepack = oc->name ()->str () == kPrepackOpName ;
263+ const auto * a = oc->args ();
264+ if (!a) {
265+ continue ;
266+ }
267+ for (unsigned j = 0 ; j < a->size (); j++) {
268+ int id = static_cast <int >(a->Get (j));
269+ if (is_prepack && j == 0 ) {
270+ prepack_src_ids.insert (id);
271+ } else if (!is_prepack) {
272+ direct_use_ids.insert (id);
273+ const auto * v = values ? values->Get (id) : nullptr ;
274+ if (v && v->value_type () == vkgraph::GraphTypes::ValueList) {
275+ const auto * items = v->value_as_ValueList ()->items ();
276+ if (items) {
277+ for (unsigned k = 0 ; k < items->size (); k++) {
278+ direct_use_ids.insert (static_cast <int >(items->Get (k)));
279+ }
280+ }
281+ }
282+ }
283+ }
284+ }
285+ }
286+
244287 for (int i = 0 ; i < num_vals; i++) {
245288 const auto * val = values->Get (i);
246289 if (!val || val->value_type () == vkgraph::GraphTypes::NONE ) {
@@ -269,60 +312,51 @@ void WebGPUGraph::build(
269312 int constant_id = vk_tensor->constant_id ();
270313 int mem_obj_id = vk_tensor->mem_obj_id ();
271314
272- // Constants always get dedicated buffers regardless of mem_obj_id
315+ // Constants are dedicated. Every constant is recorded as a
316+ // ConstantSource and materialized via materialize_constant (one
317+ // CPU->GPU write); a constant consumed ONLY via prepack is deferred
318+ // (no eager buffer -- its prepack node performs that one write).
273319 if (constant_id >= 0 || mem_obj_id < 0 ) {
274320 tensor_mem_obj_ids_[i] = -1 ;
275- WGPUBufferDescriptor buf_desc = {};
276- buf_desc.size = std::max (tensor.nbytes , size_t (4 ));
277- buf_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst |
278- WGPUBufferUsage_CopySrc;
279- buf_desc.mappedAtCreation = false ;
280- tensor.buffer = wgpuDeviceCreateBuffer (device_, &buf_desc);
281-
282- if (constant_id >= 0 && constant_data && tensor.nbytes > 0 ) {
321+
322+ if (constant_id >= 0 ) {
283323 const auto * constants = graph->constants ();
284- if (constants &&
285- constant_id < static_cast <int >(constants->size ())) {
286- const auto * vk_bytes = constants->Get (constant_id);
287- if (vk_bytes->offset () != UINT64_MAX ) {
288- const uint8_t * src = constant_data + vk_bytes->offset ();
289- wgpuQueueWriteBuffer (
290- queue_, tensor.buffer , 0 , src, tensor.nbytes );
291- } else if (
292- vk_bytes->named_key () != nullptr &&
293- named_data_map != nullptr ) {
294- // Constant stored in the PTE named-data map.
295- auto buf =
296- named_data_map->get_data (vk_bytes->named_key ()->c_str ());
297- if (!buf.ok ()) {
298- throw std::runtime_error (
299- std::string (" WebGPU: named constant '" ) +
300- vk_bytes->named_key ()->c_str () +
301- " ' not found in NamedDataMap" );
302- }
303- if (buf->size () < tensor.nbytes ) {
304- throw std::runtime_error (
305- std::string (" WebGPU: named constant '" ) +
306- vk_bytes->named_key ()->c_str () + " ' undersized: have " +
307- std::to_string (buf->size ()) + " bytes, need " +
308- std::to_string (tensor.nbytes ));
309- }
310- wgpuQueueWriteBuffer (
311- queue_, tensor.buffer , 0 , buf->data (), tensor.nbytes );
312- buf->Free ();
313- } else {
314- throw std::runtime_error (
315- " WebGPU: constant has no inline offset and no named-data key" );
316- }
317- } else {
324+ if (!constants ||
325+ constant_id >= static_cast <int >(constants->size ())) {
318326 throw std::runtime_error (
319327 " WebGPU: constant_id set but the constants table is missing "
320328 " or the id is out of range" );
321329 }
322- } else if (constant_id >= 0 && tensor.nbytes > 0 ) {
323- // constant_id set but constant_data null -> fail loud.
324- throw std::runtime_error (
325- " WebGPU: constant_id set but constant_data is null" );
330+ const auto * vk_bytes = constants->Get (constant_id);
331+ ConstantSource cs;
332+ cs.nbytes = tensor.nbytes ;
333+ if (vk_bytes->offset () != UINT64_MAX ) {
334+ cs.inline_offset = vk_bytes->offset ();
335+ } else if (vk_bytes->named_key () != nullptr ) {
336+ cs.named_key = vk_bytes->named_key ()->str ();
337+ } else {
338+ throw std::runtime_error (
339+ " WebGPU: constant has no inline offset and no named-data key" );
340+ }
341+ constant_sources_[i] = std::move (cs);
342+ }
343+
344+ // Defer constants consumed solely via prepack: skip the eager buffer.
345+ const bool defer = constant_id >= 0 &&
346+ prepack_src_ids.count (i) != 0 && direct_use_ids.count (i) == 0 ;
347+ if (!defer) {
348+ WGPUBufferDescriptor buf_desc = {};
349+ buf_desc.size = std::max (tensor.nbytes , size_t (4 ));
350+ buf_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst |
351+ WGPUBufferUsage_CopySrc;
352+ buf_desc.mappedAtCreation = false ;
353+ tensor.buffer = wgpuDeviceCreateBuffer (device_, &buf_desc);
354+
355+ // Same single CPU->GPU write the prepack node uses (no
356+ // duplication).
357+ if (constant_id >= 0 ) {
358+ materialize_constant (i, tensor.buffer );
359+ }
326360 }
327361 } else {
328362 // Shared buffer: track required size, defer allocation to pass 2
@@ -458,6 +492,47 @@ void WebGPUGraph::build(
458492 webgpu_operator_registry ().get_op_fn (op_name)(*this , args);
459493 }
460494 }
495+
496+ // Prepack nodes (Phase 3) materialized their constants directly into the
497+ // consumer buffers via materialize_constant; no separate copy pass needed.
498+ // The .pte bytes are freed right after build() returns (WebGPUBackend
499+ // processed->Free()), so clear the build-only source pointers.
500+ constant_data_ = nullptr ;
501+ named_data_map_ = nullptr ;
502+ }
503+
504+ void WebGPUGraph::materialize_constant (int const_value_id, WGPUBuffer dst) {
505+ auto it = constant_sources_.find (const_value_id);
506+ if (it == constant_sources_.end ()) {
507+ throw std::runtime_error (
508+ " WebGPU: no source recorded for constant id " +
509+ std::to_string (const_value_id));
510+ }
511+ const ConstantSource& cs = it->second ;
512+ if (cs.nbytes == 0 ) {
513+ return ;
514+ }
515+ if (cs.inline_offset != UINT64_MAX ) {
516+ if (constant_data_ == nullptr ) {
517+ throw std::runtime_error (" WebGPU: inline constant data is null" );
518+ }
519+ wgpuQueueWriteBuffer (
520+ queue_, dst, 0 , constant_data_ + cs.inline_offset , cs.nbytes );
521+ } else if (!cs.named_key .empty () && named_data_map_ != nullptr ) {
522+ auto buf = named_data_map_->get_data (cs.named_key .c_str ());
523+ if (!buf.ok ()) {
524+ throw std::runtime_error (
525+ " WebGPU: named constant '" + cs.named_key + " ' not found" );
526+ }
527+ if (buf->size () < cs.nbytes ) {
528+ throw std::runtime_error (
529+ " WebGPU: named constant '" + cs.named_key + " ' undersized" );
530+ }
531+ wgpuQueueWriteBuffer (queue_, dst, 0 , buf->data (), cs.nbytes );
532+ buf->Free ();
533+ } else {
534+ throw std::runtime_error (" WebGPU: constant has no source" );
535+ }
461536}
462537
463538WGPUShaderModule WebGPUGraph::get_or_create_shader (
@@ -780,10 +855,11 @@ WebGPUMemoryStats WebGPUGraph::memory_stats() const {
780855 for (size_t i = 0 ; i < value_types_.size (); i++) {
781856 if (value_types_[i] == ValueType::Tensor && tensors_[i].nbytes > 0 ) {
782857 stats.num_tensors ++;
783- // Shared tensors are tracked via shared_buffer_sizes_
858+ // Shared tensors are tracked via shared_buffer_sizes_; a deferred
859+ // prepack-routed constant has no buffer (no GPU memory) -> not counted.
784860 bool is_shared =
785861 i < tensor_mem_obj_ids_.size () && tensor_mem_obj_ids_[i] >= 0 ;
786- if (!is_shared) {
862+ if (!is_shared && tensors_[i]. buffer != nullptr ) {
787863 stats.unshared_tensor_buffer_bytes += tensors_[i].nbytes ;
788864 }
789865 }
0 commit comments