Skip to content

Commit ec9d6e2

Browse files
authored
[ExecuTorch][WebGPU] WGSL shader-variant codegen (vec1/vec4, fp16-ready) + rms_norm dedup (#20812)
1 parent a588b26 commit ec9d6e2

35 files changed

Lines changed: 2601 additions & 178 deletions

backends/webgpu/runtime/WebGPUBackend.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,32 @@ Result<DelegateHandle*> WebGPUBackend::init(
7979
return Error::DelegateInvalidCompatibility;
8080
}
8181

82+
// Load-time backend option (BackendOption / LoadBackendOptionsMap), keyed by
83+
// the registered backend name; default false. Mirrors the CoreML/XNNPACK
84+
// runtime-spec pattern -- no compile flag and no .pte re-export needed.
85+
bool enable_f16_kv_cache = false;
86+
{
87+
Result<bool> spec = context.get_runtime_spec<bool>("enable_f16_kv_cache");
88+
if (spec.ok()) {
89+
enable_f16_kv_cache = spec.get();
90+
}
91+
}
92+
bool enable_f16_accumulate_gemm = false;
93+
{
94+
Result<bool> spec =
95+
context.get_runtime_spec<bool>("enable_f16_accumulate_gemm");
96+
if (spec.ok()) {
97+
enable_f16_accumulate_gemm = spec.get();
98+
}
99+
}
100+
82101
try {
83-
graph->build(flatbuffer_data, constant_data, context.get_named_data_map());
102+
graph->build(
103+
flatbuffer_data,
104+
constant_data,
105+
context.get_named_data_map(),
106+
enable_f16_kv_cache,
107+
enable_f16_accumulate_gemm);
84108
} catch (const std::exception& e) {
85109
ET_LOG(Error, "WebGPU graph build failed: %s", e.what());
86110
graph->~WebGPUGraph();

backends/webgpu/runtime/WebGPUDevice.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
#include <cstdlib>
1414
#include <memory>
1515
#include <stdexcept>
16-
#ifdef WGPU_BACKEND_ENABLE_PROFILING
1716
#include <vector>
18-
#endif // WGPU_BACKEND_ENABLE_PROFILING
1917

2018
namespace executorch {
2119
namespace backends {
@@ -143,16 +141,28 @@ WebGPUContext create_webgpu_context() {
143141
device_desc.requiredLimits = &supported_limits;
144142
}
145143

144+
// Request optional features when the adapter advertises them. A feature the
145+
// adapter lacks is skipped (its fast path stays disabled). A feature the
146+
// adapter advertises becomes required of the device, so if the device were
147+
// to reject it, context creation fails below rather than falling back. The
148+
// vector must outlive wgpuAdapterRequestDevice below (device_desc points
149+
// into it).
150+
std::vector<WGPUFeatureName> required_features;
151+
if (wgpuAdapterHasFeature(ctx.adapter, WGPUFeatureName_ShaderF16)) {
152+
required_features.push_back(WGPUFeatureName_ShaderF16);
153+
ctx.shader_f16_supported = true;
154+
}
146155
#ifdef WGPU_BACKEND_ENABLE_PROFILING
147156
// Bench: enable TimestampQuery if available; fail-open (skip timing if not).
148-
std::vector<WGPUFeatureName> required_features;
149157
if (wgpuAdapterHasFeature(ctx.adapter, WGPUFeatureName_TimestampQuery)) {
150158
required_features.push_back(WGPUFeatureName_TimestampQuery);
151-
device_desc.requiredFeatureCount = required_features.size();
152-
device_desc.requiredFeatures = required_features.data();
153159
ctx.timestamp_supported = true;
154160
}
155161
#endif // WGPU_BACKEND_ENABLE_PROFILING
162+
if (!required_features.empty()) {
163+
device_desc.requiredFeatureCount = required_features.size();
164+
device_desc.requiredFeatures = required_features.data();
165+
}
156166

157167
device_desc.uncapturedErrorCallbackInfo.callback = on_device_error;
158168

backends/webgpu/runtime/WebGPUDevice.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ struct WebGPUContext {
2525
WGPUAdapter adapter = nullptr;
2626
WGPUDevice device = nullptr;
2727
WGPUQueue queue = nullptr;
28+
// True if the device was created with the ShaderF16 feature; reserved for a
29+
// future fp16 storage/compute path (fp32 is used when false or unset).
30+
bool shader_f16_supported = false;
2831
#ifdef WGPU_BACKEND_ENABLE_PROFILING
2932
// True if the device was created with the TimestampQuery feature (bench).
3033
bool timestamp_supported = false;

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,49 @@ WGPUBuffer WebGPUGraph::create_scratch_buffer(size_t nbytes) {
9090
return buffer;
9191
}
9292

93+
WGPUBuffer WebGPUGraph::acquire_scratch(size_t nbytes) {
94+
nbytes = nbytes > 0 ? nbytes : 4;
95+
// Best-fit reuse: smallest free slot with size in [nbytes, 2*nbytes] -- the
96+
// 2x cap stops a large Cmax-sized buffer from backing a tiny request. Never
97+
// reuse an in_use slot (co-live safety).
98+
ScratchSlot* best = nullptr;
99+
for (auto& s : scratch_pool_) {
100+
// s.size - nbytes (safe: s.size >= nbytes) avoids overflowing 2 * nbytes.
101+
if (!s.in_use && s.size >= nbytes && s.size - nbytes <= nbytes) {
102+
if (best == nullptr || s.size < best->size) {
103+
best = &s;
104+
}
105+
}
106+
}
107+
if (best != nullptr) {
108+
best->in_use = true;
109+
return best->buffer;
110+
}
111+
// None reusable -> create a new slot (freed in the dtor, like
112+
// scratch_buffers_).
113+
WGPUBufferDescriptor buf_desc = {};
114+
buf_desc.size = nbytes;
115+
buf_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst |
116+
WGPUBufferUsage_CopySrc;
117+
buf_desc.mappedAtCreation = false;
118+
WGPUBuffer buffer = wgpuDeviceCreateBuffer(device_, &buf_desc);
119+
scratch_pool_.push_back({buffer, nbytes, true});
120+
return buffer;
121+
}
122+
123+
void WebGPUGraph::release_scratch(WGPUBuffer buffer) {
124+
if (!buffer) {
125+
return;
126+
}
127+
for (auto& s : scratch_pool_) {
128+
if (s.buffer == buffer) {
129+
s.in_use = false;
130+
return;
131+
}
132+
}
133+
// Not a pooled buffer -> no-op; the dtor frees it via scratch_buffers_.
134+
}
135+
93136
WGPUBuffer WebGPUGraph::make_uniform_buffer(const void* data, size_t size) {
94137
WGPUBufferDescriptor desc = {};
95138
desc.size = size;
@@ -267,6 +310,11 @@ WebGPUGraph::~WebGPUGraph() {
267310
wgpuBufferRelease(buf);
268311
}
269312
}
313+
for (auto& s : scratch_pool_) {
314+
if (s.buffer) {
315+
wgpuBufferRelease(s.buffer);
316+
}
317+
}
270318
for (auto& buf : owned_uniform_buffers_) {
271319
if (buf) {
272320
wgpuBufferRelease(buf);
@@ -310,7 +358,9 @@ WebGPUGraph::~WebGPUGraph() {
310358
void WebGPUGraph::build(
311359
const void* flatbuffer_data,
312360
const uint8_t* constant_data,
313-
const executorch::runtime::NamedDataMap* named_data_map) {
361+
const executorch::runtime::NamedDataMap* named_data_map,
362+
bool f16_kv_cache,
363+
bool f16_accumulate_gemm) {
314364
if (!device_) {
315365
auto* ctx = get_default_webgpu_context();
316366
if (ctx) {
@@ -331,6 +381,15 @@ void WebGPUGraph::build(
331381
constant_data_ = constant_data;
332382
named_data_map_ = named_data_map;
333383

384+
// f16 KV cache (runtime opt-in): store K/V caches as f16 iff the opt-in is
385+
// set AND the device negotiated shader-f16 (fail-closed).
386+
const WebGPUContext* kv_ctx = get_default_webgpu_context();
387+
kv_f16_ = f16_kv_cache && (kv_ctx != nullptr && kv_ctx->shader_f16_supported);
388+
389+
// f16-accumulate q4gsw steel prefill GEMM (runtime opt-in). QuantizedLinear
390+
// additionally gates the kernel on the negotiated shader-f16 feature.
391+
f16_accumulate_gemm_ = f16_accumulate_gemm;
392+
334393
// Phase 1: Create all values
335394
const auto* values = graph->values();
336395
const int num_vals = values ? values->size() : 0;
@@ -358,6 +417,13 @@ void WebGPUGraph::build(
358417
if (!a) {
359418
continue;
360419
}
420+
// f16 KV: tag sdpa K/V cache values (args[3],[4]) for half-size alloc.
421+
// Inert unless kv_f16_ (runtime opt-in) is set.
422+
if (kv_f16_ && a->size() > 4 &&
423+
oc->name()->str() == "sdpa_with_kv_cache.default") {
424+
kv_cache_ids_.insert(static_cast<int>(a->Get(3)));
425+
kv_cache_ids_.insert(static_cast<int>(a->Get(4)));
426+
}
361427
for (unsigned j = 0; j < a->size(); j++) {
362428
int id = static_cast<int>(a->Get(j));
363429
if (is_prepack && j == 0) {
@@ -378,6 +444,29 @@ void WebGPUGraph::build(
378444
}
379445
}
380446

447+
// f16 KV defensive guard: fail loud if a non-sdpa op reads an f16 cache.
448+
// Inert unless kv_f16_ (runtime opt-in) is set.
449+
if (kv_f16_ && !kv_cache_ids_.empty() && chain_prescan) {
450+
for (unsigned ci = 0; ci < chain_prescan->size(); ci++) {
451+
const auto* oc = chain_prescan->Get(ci);
452+
const std::string nm = oc->name()->str();
453+
if (nm == "sdpa_with_kv_cache.default" || nm == kPrepackOpName) {
454+
continue;
455+
}
456+
const auto* a = oc->args();
457+
if (!a) {
458+
continue;
459+
}
460+
for (unsigned j = 0; j < a->size(); j++) {
461+
if (kv_cache_ids_.count(static_cast<int>(a->Get(j))) != 0) {
462+
throw std::runtime_error(
463+
"WebGPU f16 KV: cache tensor consumed by non-sdpa op '" + nm +
464+
"' would misread the f16 buffer");
465+
}
466+
}
467+
}
468+
}
469+
381470
for (int i = 0; i < num_vals; i++) {
382471
const auto* val = values->Get(i);
383472
if (!val || val->value_type() == vkgraph::GraphTypes::NONE) {
@@ -407,6 +496,23 @@ void WebGPUGraph::build(
407496
tensor.cur_dims = tensor.dims;
408497
tensor.cur_nbytes = tensor.nbytes;
409498

499+
// f16 KV cache: dedicated half-size array<f16> buffer. WebGPU
500+
// zero-initializes freshly-created buffers, so no explicit clear is
501+
// needed. Inert unless kv_f16_ (runtime opt-in) is set.
502+
if (kv_f16_ && kv_cache_ids_.count(i) != 0) {
503+
tensor.elem_size = 2;
504+
tensor.nbytes = numel * 2;
505+
tensor.cur_nbytes = tensor.nbytes;
506+
tensor_mem_obj_ids_[i] = -1;
507+
WGPUBufferDescriptor buf_desc = {};
508+
buf_desc.size = std::max(tensor.nbytes, size_t(4));
509+
buf_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst |
510+
WGPUBufferUsage_CopySrc;
511+
buf_desc.mappedAtCreation = false;
512+
tensor.buffer = wgpuDeviceCreateBuffer(device_, &buf_desc);
513+
break;
514+
}
515+
410516
int constant_id = vk_tensor->constant_id();
411517
int mem_obj_id = vk_tensor->mem_obj_id();
412518

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ class WebGPUGraph {
104104
void build(
105105
const void* flatbuffer_data,
106106
const uint8_t* constant_data,
107-
const executorch::runtime::NamedDataMap* named_data_map = nullptr);
107+
const executorch::runtime::NamedDataMap* named_data_map = nullptr,
108+
bool f16_kv_cache = false,
109+
bool f16_accumulate_gemm = false);
108110

109111
// Copy input tensor data from host pointers into GPU buffers.
110112
void copy_inputs(const std::vector<InputData>& inputs);
@@ -267,6 +269,35 @@ class WebGPUGraph {
267269
// Graph-owned scratch storage buffer for fused-op intermediates (e.g. SDPA).
268270
WGPUBuffer create_scratch_buffer(size_t nbytes);
269271

272+
// Reusable scratch pool for SINGLE-OP-LIFETIME fused-op scratch (SDPA
273+
// attn_weights/softmax, FlashDecoding partials). acquire_scratch() reuses a
274+
// free slot (best-fit, size in [n,2n]) or creates one; the caller RELEASES it
275+
// at op-lowering scope exit (use ScopedScratch), so N layers' scratch reuses
276+
// a small constant of buffers instead of N x held to graph teardown.
277+
// Correctness: WebGPU/Dawn auto-inserts RAW hazard barriers between
278+
// dispatches on a shared storage buffer regardless of pass structure -- the
279+
// SAME guarantee mem_obj_id aliasing already relies on -- so reuse is
280+
// bit-identical. Never hand a still-in_use slot to a co-live requester.
281+
WGPUBuffer acquire_scratch(size_t nbytes);
282+
void release_scratch(WGPUBuffer buffer);
283+
// RAII: releases an acquired scratch slot when the op-lowering scope exits
284+
// (leak-safe vs early returns).
285+
struct ScopedScratch {
286+
WebGPUGraph* g = nullptr;
287+
WGPUBuffer buf = nullptr;
288+
ScopedScratch(WebGPUGraph* graph, WGPUBuffer b) : g(graph), buf(b) {}
289+
~ScopedScratch() {
290+
if (g && buf) {
291+
g->release_scratch(buf);
292+
}
293+
}
294+
ScopedScratch(const ScopedScratch&) = delete;
295+
ScopedScratch& operator=(const ScopedScratch&) = delete;
296+
operator WGPUBuffer() const {
297+
return buf;
298+
}
299+
};
300+
270301
// Create a mapped-at-creation uniform buffer from `size` bytes and track it
271302
// in the memory stats. Shared helper for ops needing a uniform Params buffer.
272303
WGPUBuffer make_uniform_buffer(const void* data, size_t size);
@@ -314,6 +345,23 @@ class WebGPUGraph {
314345
return value_types_[id];
315346
}
316347

348+
public:
349+
// True when the sdpa K/V cache is stored f16-packed (runtime opt-in).
350+
bool kv_f16() const {
351+
return kv_f16_;
352+
}
353+
354+
// True when the q4gsw steel prefill GEMM uses the lossy f16-accumulate kernel
355+
// (runtime opt-in; perplexity-gated, not bit-exact).
356+
bool f16_accumulate_gemm() const {
357+
return f16_accumulate_gemm_;
358+
}
359+
360+
private:
361+
bool kv_f16_ = false;
362+
std::unordered_set<int> kv_cache_ids_;
363+
bool f16_accumulate_gemm_ = false;
364+
317365
private:
318366
WGPUInstance instance_ = nullptr;
319367
WGPUDevice device_ = nullptr;
@@ -366,6 +414,16 @@ class WebGPUGraph {
366414
// Long-lived scratch storage buffers for fused ops (e.g. SDPA temporaries).
367415
std::vector<WGPUBuffer> scratch_buffers_;
368416

417+
// Reusable scratch pool: single-op-lifetime buffers recycled across ops
418+
// (acquire_scratch/release_scratch). Each slot is freed in the dtor. See
419+
// acquire_scratch() for the reuse policy.
420+
struct ScratchSlot {
421+
WGPUBuffer buffer = nullptr;
422+
size_t size = 0;
423+
bool in_use = false;
424+
};
425+
std::vector<ScratchSlot> scratch_pool_;
426+
369427
// Uniform buffers owned for the graph's lifetime; released in the dtor.
370428
std::vector<WGPUBuffer> owned_uniform_buffers_;
371429

0 commit comments

Comments
 (0)