@@ -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+
93136WGPUBuffer 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() {
310358void 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
0 commit comments