Skip to content

Commit e53fa18

Browse files
committed
[ExecuTorch][WebGPU] Add Qwen3 K16 attention and fp16 KV cache
Pull Request resolved: #21136 Qwen3's attention geometry differs from Llama's, and its KV cache is produced in fp32 on the host but must be consumed in fp16 on the device. This adds guarded Qwen3 K16 streaming online-softmax attention schedules — a Q16 schedule that is the automatic default whenever the exact geometry and capability guards pass, plus a Q32 candidate that is opt-in through the `sdpa_query_tile` runtime spec (BackendOption) for future autotuning — together with the exact fp32-host to fp16-device KV-cache boundary conversion. Selection requires the exact Qwen3 geometry, fp16 KV storage, adapter limits, a valid workgroup count, and an exact 2:1 byte ratio; the established Llama, materialized, and FlashDecoding routes remain fallbacks. This builds on the HuggingFace rotate-half RoPE operator. No Vulkan analogue (WebGPU-specific online-softmax attention; Vulkan has only a materialized attention). It also makes the long generated WGSL provenance and constant declarations format-stable and covers them with a generator regression test. Key changes: - runtime/ops/sdpa/streaming_attention_qwen3_k16_causal_bound.wgsl and streaming_attention_qwen3_q32_k16_causal_bound.wgsl (+ generated headers): the Q16 and Q32 online-softmax Qwen3 kernels. - Sdpa.cpp, WebGPUGraph.{cpp,h}: exact Qwen3 geometry and limit guards, Q16 default route selection, and the fp32-host to fp16-device KV-cache conversion. - WebGPUBackend.cpp: read the optional `sdpa_query_tile` runtime spec and thread it into graph build so the Q32 tile can be requested without a rebuild. - scripts/gen_wgsl_headers.py (+ test_wgsl_codegen.py): format-stable generated headers with a regression test. ghstack-source-id: 411961459 @exported-using-ghexport Differential Revision: [D113171744](https://our.internmc.facebook.com/intern/diff/D113171744/)
1 parent 2246c4c commit e53fa18

19 files changed

Lines changed: 2488 additions & 122 deletions

backends/webgpu/runtime/WebGPUBackend.cpp

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Result<DelegateHandle*> WebGPUBackend::init(
9090

9191
// Parse header to locate flatbuffer and constant data
9292
Result<WebGPUDelegateHeader> header =
93-
WebGPUDelegateHeader::parse(processed->data());
93+
WebGPUDelegateHeader::parse(processed->data(), processed->size());
9494
if (!header.ok()) {
9595
ET_LOG(Error, "WebGPUDelegateHeader may be corrupt");
9696
return header.error();
@@ -101,6 +101,17 @@ Result<DelegateHandle*> WebGPUBackend::init(
101101
const uint8_t* flatbuffer_data = buffer_start + header->flatbuffer_offset;
102102
const uint8_t* constant_data = buffer_start + header->bytes_offset;
103103

104+
size_t constant_data_size = header->bytes_size;
105+
if (constant_data_size == 0 && processed->size() > header->bytes_offset) {
106+
constant_data_size = processed->size() - header->bytes_offset;
107+
}
108+
109+
flatbuffers::Verifier verifier(flatbuffer_data, header->flatbuffer_size);
110+
if (!vkgraph::VerifyVkGraphBuffer(verifier)) {
111+
ET_LOG(Error, "WebGPU delegate FlatBuffer verification failed");
112+
return Error::DelegateInvalidCompatibility;
113+
}
114+
104115
// Verify FlatBuffer identifier
105116
if (!vkgraph::VkGraphBufferHasIdentifier(flatbuffer_data)) {
106117
ET_LOG(
@@ -125,10 +136,20 @@ Result<DelegateHandle*> WebGPUBackend::init(
125136
config.f16_accumulate_gemm = spec.get();
126137
}
127138
}
139+
{
140+
Result<int> spec = context.get_runtime_spec<int>("sdpa_query_tile");
141+
if (spec.ok()) {
142+
config.sdpa_query_tile = spec.get();
143+
}
144+
}
128145

129146
try {
130147
graph->build(
131-
flatbuffer_data, constant_data, context.get_named_data_map(), config);
148+
flatbuffer_data,
149+
constant_data,
150+
constant_data_size,
151+
context.get_named_data_map(),
152+
config);
132153
} catch (const std::exception& e) {
133154
ET_LOG(Error, "WebGPU graph build failed: %s", e.what());
134155
graph->~WebGPUGraph();
@@ -163,8 +184,13 @@ Error WebGPUBackend::execute(
163184
const auto& tensor = args[i]->toTensor();
164185
const bool host_is_int64 =
165186
tensor.scalar_type() == executorch::aten::ScalarType::Long;
187+
const bool host_is_fp32 =
188+
tensor.scalar_type() == executorch::aten::ScalarType::Float;
166189
inputs.push_back(
167-
{tensor.const_data_ptr(), tensor.nbytes(), host_is_int64});
190+
{tensor.const_data_ptr(),
191+
tensor.nbytes(),
192+
host_is_int64,
193+
host_is_fp32});
168194
const auto sizes = tensor.sizes();
169195
std::vector<int64_t> new_dims(sizes.begin(), sizes.end());
170196
graph->resize_input(graph->input_ids()[i], new_dims);
@@ -205,12 +231,15 @@ Error WebGPUBackend::execute(
205231
graph->execute(plan);
206232

207233
// Copy outputs from GPU staging buffers to EValue tensor data pointers
208-
std::vector<std::pair<void*, size_t>> outputs;
234+
std::vector<OutputData> outputs;
209235
outputs.reserve(num_outputs);
210236
for (size_t i = 0; i < num_outputs; i++) {
211237
const size_t arg_idx = num_inputs + i;
212238
auto& tensor = args[arg_idx]->toTensor();
213-
outputs.emplace_back(tensor.mutable_data_ptr(), tensor.nbytes());
239+
const bool host_is_fp32 =
240+
tensor.scalar_type() == executorch::aten::ScalarType::Float;
241+
outputs.push_back(
242+
{tensor.mutable_data_ptr(), tensor.nbytes(), host_is_fp32});
214243
}
215244
graph->copy_outputs(outputs, plan);
216245
} catch (const std::exception& e) {

backends/webgpu/runtime/WebGPUDelegateHeader.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,19 @@ bool WebGPUDelegateHeader::is_valid() const {
6565
if (flatbuffer_size == 0) {
6666
return false;
6767
}
68-
if (bytes_offset < flatbuffer_offset + flatbuffer_size) {
68+
if (bytes_offset < flatbuffer_offset ||
69+
flatbuffer_size > bytes_offset - flatbuffer_offset) {
6970
return false;
7071
}
7172
return true;
7273
}
7374

74-
Result<WebGPUDelegateHeader> WebGPUDelegateHeader::parse(const void* data) {
75+
Result<WebGPUDelegateHeader> WebGPUDelegateHeader::parse(
76+
const void* data,
77+
size_t buffer_size) {
78+
if (data == nullptr || buffer_size < kExpectedSize) {
79+
return Error::InvalidArgument;
80+
}
7581
const uint8_t* header_data = (const uint8_t*)data;
7682

7783
const uint8_t* magic_start = header_data + kMagic.offset;
@@ -91,6 +97,13 @@ Result<WebGPUDelegateHeader> WebGPUDelegateHeader::parse(const void* data) {
9197
return Error::InvalidArgument;
9298
}
9399

100+
if (header.flatbuffer_offset > buffer_size ||
101+
header.flatbuffer_size > buffer_size - header.flatbuffer_offset ||
102+
header.bytes_offset > buffer_size ||
103+
header.bytes_size > buffer_size - header.bytes_offset) {
104+
return Error::InvalidArgument;
105+
}
106+
94107
return header;
95108
}
96109

backends/webgpu/runtime/WebGPUDelegateHeader.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
#pragma once
1010

11+
#include <cstddef>
12+
1113
#include <executorch/runtime/core/result.h>
1214

1315
namespace executorch {
@@ -18,7 +20,8 @@ struct WebGPUDelegateHeader {
1820
bool is_valid() const;
1921

2022
static executorch::runtime::Result<WebGPUDelegateHeader> parse(
21-
const void* data);
23+
const void* data,
24+
size_t buffer_size);
2225

2326
uint32_t header_size;
2427
uint32_t flatbuffer_offset;

0 commit comments

Comments
 (0)