Skip to content

Commit 7a66f0a

Browse files
authored
[ExecuTorch][WebGPU] Add et_vk.prepack (constant-tensor packing) for E2E weight loading
Differential Revision: D108428754 Pull Request resolved: #20265
1 parent ce2814c commit 7a66f0a

4 files changed

Lines changed: 202 additions & 49 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ set(WEBGPU_SRCS
4141
runtime/ops/mul/BinaryOp.cpp
4242
runtime/ops/embedding_q4gsw/EmbeddingQ4gsw.cpp
4343
runtime/ops/rope/RotaryEmbedding.cpp
44+
runtime/ops/prepack/Prepack.cpp
4445
)
4546

4647
add_library(webgpu_backend ${WEBGPU_SRCS})

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 125 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ namespace executorch::backends::webgpu {
2626

2727
namespace {
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+
2933
size_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

463538
WGPUShaderModule 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
}

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ struct OutputCopy {
5050
size_t nbytes = 0;
5151
};
5252

53+
// CPU-side record for a prepack-routed constant; mirrors Vulkan's TensorRef
54+
// (sizes + a data reference, not a live GPU tensor). The prepack node is the
55+
// sole materialization, so the constant needs no eager GPU buffer.
56+
struct ConstantSource {
57+
uint64_t inline_offset = UINT64_MAX; // offset into constant_data_; else key
58+
std::string named_key; // non-empty => fetch from named_data_map_
59+
size_t nbytes = 0;
60+
};
61+
5362
struct ExecuteConfig {
5463
size_t chunk_size = 0;
5564
size_t initial_chunk_size = 0;
@@ -180,6 +189,11 @@ class WebGPUGraph {
180189
dispatches_.push_back(dispatch);
181190
}
182191

192+
// Materialize a recorded prepack-routed constant into dst via one CPU->GPU
193+
// transfer. Build-time only (the .pte bytes are freed after build()).
194+
// Mirrors Vulkan prepack_standard.
195+
void materialize_constant(int const_value_id, WGPUBuffer dst);
196+
183197
void add_uniform_buffer_bytes(size_t bytes) {
184198
uniform_buffer_bytes_ += bytes;
185199
}
@@ -286,6 +300,13 @@ class WebGPUGraph {
286300

287301
std::vector<WebGPUDispatch> dispatches_;
288302

303+
// Prepack-routed constant sources (offset/named-key + size); the prepack node
304+
// materializes these once. constant_data_/named_data_map_ point at the .pte
305+
// bytes and are valid only during build().
306+
const uint8_t* constant_data_ = nullptr;
307+
const executorch::runtime::NamedDataMap* named_data_map_ = nullptr;
308+
std::unordered_map<int, ConstantSource> constant_sources_;
309+
289310
ExecuteConfig execute_config_;
290311

291312
// Caches for reusing GPU objects across dispatches.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
10+
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
11+
12+
#include <stdexcept>
13+
14+
namespace executorch::backends::webgpu {
15+
16+
namespace {
17+
18+
// Materialize a constant into the prepack-output buffer via one CPU->GPU write.
19+
void prepack_impl(WebGPUGraph& graph, const std::vector<int>& args) {
20+
// et_vk.prepack.default args: [src (constant), out].
21+
if (args.size() != 2) {
22+
throw std::runtime_error("WebGPU prepack: expected 2 args (src, out)");
23+
}
24+
const auto& src = graph.get_tensor(args.at(0));
25+
const auto& out = graph.get_tensor(args.at(1));
26+
27+
if (src.dims != out.dims) {
28+
throw std::runtime_error("WebGPU prepack: src/out shape mismatch");
29+
}
30+
if (src.elem_size != out.elem_size) {
31+
throw std::runtime_error(
32+
"WebGPU prepack: src/out dtype mismatch (cast unsupported)");
33+
}
34+
if (src.nbytes != out.nbytes) {
35+
throw std::runtime_error("WebGPU prepack: src/out byte-size mismatch");
36+
}
37+
if (out.buffer == nullptr) {
38+
throw std::runtime_error("WebGPU prepack: null out buffer binding");
39+
}
40+
41+
// Sole materialization: write the .pte bytes once, straight into the
42+
// consumer's buffer (no eager src buffer, no buffer->buffer copy).
43+
// Correctness of this write-once relies on `out` being a dedicated buffer
44+
// (the partitioner gives prepack outputs mem_obj_id=-1, so it is never
45+
// memory-plan aliased with a transient that execute() would later overwrite).
46+
graph.materialize_constant(args.at(0), out.buffer);
47+
}
48+
49+
} // namespace
50+
51+
WEBGPU_REGISTER_OPERATORS {
52+
WEBGPU_REGISTER_OP(et_vk.prepack.default, prepack_impl);
53+
}
54+
55+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)