Skip to content

Commit ba07493

Browse files
authored
[ExecuTorch][WebGPU] Add et_vk.apply_rotary_emb (interleaved RoPE) + ValueList multi-output
Differential Revision: D108428756 Pull Request resolved: #20264
1 parent 33f7da6 commit ba07493

6 files changed

Lines changed: 439 additions & 1 deletion

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ set(WEBGPU_SRCS
4040
runtime/ops/quantized_linear/QuantizedLinear.cpp
4141
runtime/ops/mul/BinaryOp.cpp
4242
runtime/ops/embedding_q4gsw/EmbeddingQ4gsw.cpp
43+
runtime/ops/rope/RotaryEmbedding.cpp
4344
)
4445

4546
add_library(webgpu_backend ${WEBGPU_SRCS})

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ void WebGPUGraph::build(
239239
ints_.resize(num_vals, 0);
240240
doubles_.resize(num_vals, 0.0);
241241
bools_.resize(num_vals, false);
242+
value_lists_.resize(num_vals);
242243

243244
for (int i = 0; i < num_vals; i++) {
244245
const auto* val = values->Get(i);
@@ -313,7 +314,15 @@ void WebGPUGraph::build(
313314
throw std::runtime_error(
314315
"WebGPU: constant has no inline offset and no named-data key");
315316
}
317+
} else {
318+
throw std::runtime_error(
319+
"WebGPU: constant_id set but the constants table is missing "
320+
"or the id is out of range");
316321
}
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");
317326
}
318327
} else {
319328
// Shared buffer: track required size, defer allocation to pass 2
@@ -363,6 +372,16 @@ void WebGPUGraph::build(
363372
add_uniform_buffer_bytes(kSymIntUniformBytes);
364373
break;
365374
}
375+
case vkgraph::GraphTypes::ValueList: {
376+
value_types_[i] = ValueType::ValueList;
377+
const auto* items = val->value_as_ValueList()->items();
378+
if (items) {
379+
for (unsigned j = 0; j < items->size(); j++) {
380+
value_lists_[i].push_back(static_cast<int>(items->Get(j)));
381+
}
382+
}
383+
break;
384+
}
366385
default:
367386
value_types_[i] = ValueType::Null;
368387
break;

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ class WebGPUGraph {
119119
bool get_bool(int id) const {
120120
return bools_[id];
121121
}
122+
// Member value ids of a serialized ValueList (op multi-output list).
123+
const std::vector<int>& get_value_list(int id) const {
124+
return value_lists_[id];
125+
}
122126

123127
// Live-scalar (SymInt) API; mirrors the Vulkan SymInt/ParamsBuffer UBO.
124128
// set_symint writes the buffer + marks dirty only if the value changed.
@@ -215,7 +219,16 @@ class WebGPUGraph {
215219
return static_cast<int>(value_types_.size());
216220
}
217221

218-
enum class ValueType { Tensor, Int, Double, Bool, Null, String, SymInt };
222+
enum class ValueType {
223+
Tensor,
224+
Int,
225+
Double,
226+
Bool,
227+
Null,
228+
String,
229+
SymInt,
230+
ValueList
231+
};
219232

220233
ValueType get_value_type(int id) const {
221234
return value_types_[id];
@@ -233,6 +246,7 @@ class WebGPUGraph {
233246
std::vector<int64_t> ints_;
234247
std::vector<double> doubles_;
235248
std::vector<bool> bools_;
249+
std::vector<std::vector<int>> value_lists_;
236250

237251
// SymInt (live scalar): id -> {live Uniform buffer, current value}, sparse.
238252
struct SymIntSlot {
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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/WebGPUUtils.h>
11+
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
12+
#include <executorch/backends/webgpu/runtime/ops/rope/rotary_embedding_wgsl.h>
13+
14+
#include <webgpu/webgpu.h>
15+
16+
#include <cstdint>
17+
#include <cstring>
18+
#include <stdexcept>
19+
20+
namespace executorch::backends::webgpu {
21+
22+
namespace {
23+
24+
// Uniform layout matching the WGSL Params struct (16-byte aligned, 32 bytes).
25+
struct RotaryParams {
26+
uint32_t n_heads;
27+
uint32_t seq;
28+
uint32_t head_dim;
29+
uint32_t half_dim;
30+
uint32_t num_pairs;
31+
uint32_t _pad0;
32+
uint32_t _pad1;
33+
uint32_t _pad2;
34+
};
35+
static_assert(sizeof(RotaryParams) == 32, "RotaryParams must be 32 bytes");
36+
37+
uint64_t numel_of(const std::vector<int64_t>& dims) {
38+
uint64_t n = 1;
39+
for (int64_t d : dims) {
40+
n *= static_cast<uint64_t>(d);
41+
}
42+
return n;
43+
}
44+
45+
// Rotate one (x->out) with the shared shader; freqs shared between xq and xk.
46+
void add_rope_dispatch(
47+
WebGPUGraph& graph,
48+
WGPUDevice device,
49+
WGPUComputePipeline pipeline,
50+
WGPUBindGroupLayout bgl,
51+
const WebGPUTensor& x,
52+
const WebGPUTensor& out,
53+
const WebGPUTensor& freqs_cos,
54+
const WebGPUTensor& freqs_sin,
55+
uint32_t n_heads,
56+
uint32_t seq,
57+
uint32_t head_dim,
58+
uint32_t workgroup_count) {
59+
const uint32_t half_dim = head_dim / 2u;
60+
// out.dims == in.dims (asserted in impl), so this matches the caller's wgc.
61+
const uint32_t num_pairs = static_cast<uint32_t>(numel_of(out.dims) / 2u);
62+
63+
RotaryParams params = {};
64+
params.n_heads = n_heads;
65+
params.seq = seq;
66+
params.head_dim = head_dim;
67+
params.half_dim = half_dim;
68+
params.num_pairs = num_pairs;
69+
70+
WGPUBufferDescriptor uniform_desc = {};
71+
uniform_desc.size = sizeof(RotaryParams);
72+
uniform_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
73+
uniform_desc.mappedAtCreation = true;
74+
WGPUBuffer uniform_buffer = wgpuDeviceCreateBuffer(device, &uniform_desc);
75+
void* mapped =
76+
wgpuBufferGetMappedRange(uniform_buffer, 0, sizeof(RotaryParams));
77+
std::memcpy(mapped, &params, sizeof(RotaryParams));
78+
wgpuBufferUnmap(uniform_buffer);
79+
graph.add_uniform_buffer_bytes(sizeof(RotaryParams));
80+
81+
WGPUBindGroupEntry bg_entries[5] = {};
82+
bg_entries[0].binding = 0;
83+
bg_entries[0].buffer = out.buffer;
84+
bg_entries[0].size = out.nbytes;
85+
bg_entries[1].binding = 1;
86+
bg_entries[1].buffer = x.buffer;
87+
bg_entries[1].size = x.nbytes;
88+
bg_entries[2].binding = 2;
89+
bg_entries[2].buffer = freqs_cos.buffer;
90+
bg_entries[2].size = freqs_cos.nbytes;
91+
bg_entries[3].binding = 3;
92+
bg_entries[3].buffer = freqs_sin.buffer;
93+
bg_entries[3].size = freqs_sin.nbytes;
94+
bg_entries[4].binding = 4;
95+
bg_entries[4].buffer = uniform_buffer;
96+
bg_entries[4].size = sizeof(RotaryParams);
97+
98+
WGPUBindGroupDescriptor bg_desc = {};
99+
bg_desc.layout = bgl;
100+
bg_desc.entryCount = 5;
101+
bg_desc.entries = bg_entries;
102+
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
103+
104+
graph.add_dispatch(
105+
{pipeline, bind_group, workgroup_count, "apply_rotary_emb"});
106+
107+
wgpuBufferRelease(uniform_buffer);
108+
}
109+
110+
// args: [xq, xk, freqs_cos, freqs_sin, out_list(ValueList[xq_out, xk_out])].
111+
void apply_rotary_emb_impl(WebGPUGraph& graph, const std::vector<int>& args) {
112+
const int xq_id = args.at(0);
113+
const int xk_id = args.at(1);
114+
const int freqs_cos_id = args.at(2);
115+
const int freqs_sin_id = args.at(3);
116+
117+
const std::vector<int>& out_list = graph.get_value_list(args.at(4));
118+
if (out_list.size() != 2) {
119+
throw std::runtime_error(
120+
"WebGPU apply_rotary_emb: expected an output ValueList of size 2");
121+
}
122+
123+
WGPUDevice device = graph.device();
124+
125+
const auto& xq = graph.get_tensor(xq_id);
126+
const auto& xk = graph.get_tensor(xk_id);
127+
const auto& freqs_cos = graph.get_tensor(freqs_cos_id);
128+
const auto& freqs_sin = graph.get_tensor(freqs_sin_id);
129+
const auto& xq_out = graph.get_tensor(out_list[0]);
130+
const auto& xk_out = graph.get_tensor(out_list[1]);
131+
132+
// Vulkan shape contract: xq/xk (B,S,n_heads,head_dim), freqs (S,head_dim/2).
133+
if (xq.dims.size() < 3 || xk.dims.size() < 3 || freqs_cos.dims.size() < 2) {
134+
throw std::runtime_error("WebGPU apply_rotary_emb: malformed dims");
135+
}
136+
const uint32_t head_dim = static_cast<uint32_t>(xq.dims.back());
137+
const uint32_t seq = static_cast<uint32_t>(xq.dims[xq.dims.size() - 3]);
138+
const uint32_t n_heads_q = static_cast<uint32_t>(xq.dims[xq.dims.size() - 2]);
139+
const uint32_t n_heads_k = static_cast<uint32_t>(xk.dims[xk.dims.size() - 2]);
140+
const uint32_t seq_k = static_cast<uint32_t>(xk.dims[xk.dims.size() - 3]);
141+
const uint32_t half_dim = static_cast<uint32_t>(freqs_cos.dims.back());
142+
143+
if (head_dim == 0 || head_dim % 2 != 0) {
144+
throw std::runtime_error(
145+
"WebGPU apply_rotary_emb: head_dim must be a nonzero multiple of 2");
146+
}
147+
if (static_cast<uint32_t>(xk.dims.back()) != head_dim || seq_k != seq) {
148+
throw std::runtime_error(
149+
"WebGPU apply_rotary_emb: xq/xk head_dim and seq must match");
150+
}
151+
if (half_dim * 2u != head_dim) {
152+
throw std::runtime_error(
153+
"WebGPU apply_rotary_emb: head_dim != 2 * freqs_cos last dim");
154+
}
155+
if (freqs_cos.dims != freqs_sin.dims) {
156+
throw std::runtime_error(
157+
"WebGPU apply_rotary_emb: freqs_cos and freqs_sin shapes differ");
158+
}
159+
160+
if (xq.buffer == nullptr || xk.buffer == nullptr ||
161+
freqs_cos.buffer == nullptr || freqs_sin.buffer == nullptr ||
162+
xq_out.buffer == nullptr || xk_out.buffer == nullptr) {
163+
throw std::runtime_error("WebGPU apply_rotary_emb: null buffer binding");
164+
}
165+
166+
// All tensors are fp32; output shapes equal their inputs.
167+
const uint64_t xq_numel = numel_of(xq.dims);
168+
const uint64_t xk_numel = numel_of(xk.dims);
169+
const uint64_t freqs_numel = numel_of(freqs_cos.dims);
170+
if (freqs_numel != static_cast<uint64_t>(seq) * half_dim ||
171+
xq.nbytes != xq_numel * sizeof(float) ||
172+
xk.nbytes != xk_numel * sizeof(float) ||
173+
freqs_cos.nbytes != freqs_numel * sizeof(float) ||
174+
freqs_sin.nbytes != freqs_numel * sizeof(float) ||
175+
xq_out.nbytes != xq_numel * sizeof(float) ||
176+
xk_out.nbytes != xk_numel * sizeof(float)) {
177+
throw std::runtime_error(
178+
"WebGPU apply_rotary_emb: dtype/byte-size mismatch (all fp32) or "
179+
"freqs shape != [seq, head_dim/2]");
180+
}
181+
182+
if (xq_numel / 2u > UINT32_MAX || xk_numel / 2u > UINT32_MAX) {
183+
throw std::runtime_error(
184+
"WebGPU apply_rotary_emb: pair count exceeds uint32 dispatch range");
185+
}
186+
187+
const uint32_t wg_size =
188+
utils::clamp_workgroup_size(device, kRotaryEmbeddingWorkgroupSizeX);
189+
// Validate both dispatches before any GPU-object alloc (no leak on throw).
190+
const uint32_t xq_wgc = utils::compute_1d_workgroup_count(
191+
device,
192+
static_cast<uint32_t>(xq_numel / 2u),
193+
wg_size,
194+
"apply_rotary_emb");
195+
const uint32_t xk_wgc = utils::compute_1d_workgroup_count(
196+
device,
197+
static_cast<uint32_t>(xk_numel / 2u),
198+
wg_size,
199+
"apply_rotary_emb");
200+
201+
WGPUShaderSourceWGSL wgsl_desc = {};
202+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
203+
wgsl_desc.code = {kRotaryEmbeddingWGSL, WGPU_STRLEN};
204+
WGPUShaderModuleDescriptor shader_desc = {};
205+
shader_desc.nextInChain = &wgsl_desc.chain;
206+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
207+
208+
// Bind group: out (rw) + in/freqs_cos/freqs_sin (ro) + uniform.
209+
WGPUBindGroupLayoutEntry entries[5] = {};
210+
entries[0].binding = 0;
211+
entries[0].visibility = WGPUShaderStage_Compute;
212+
entries[0].buffer.type = WGPUBufferBindingType_Storage;
213+
for (uint32_t i = 1; i <= 3; i++) {
214+
entries[i].binding = i;
215+
entries[i].visibility = WGPUShaderStage_Compute;
216+
entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
217+
}
218+
entries[4].binding = 4;
219+
entries[4].visibility = WGPUShaderStage_Compute;
220+
entries[4].buffer.type = WGPUBufferBindingType_Uniform;
221+
222+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
223+
bgl_desc.entryCount = 5;
224+
bgl_desc.entries = entries;
225+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
226+
227+
WGPUPipelineLayoutDescriptor pl_desc = {};
228+
pl_desc.bindGroupLayoutCount = 1;
229+
pl_desc.bindGroupLayouts = &bgl;
230+
WGPUPipelineLayout pipeline_layout =
231+
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
232+
233+
WGPUConstantEntry wg_size_constant = {};
234+
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
235+
wg_size_constant.value = static_cast<double>(wg_size);
236+
237+
WGPUComputePipelineDescriptor pipeline_desc = {};
238+
pipeline_desc.layout = pipeline_layout;
239+
pipeline_desc.compute.module = shader;
240+
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
241+
pipeline_desc.compute.constantCount = 1;
242+
pipeline_desc.compute.constants = &wg_size_constant;
243+
// One pipeline per dispatch; a shared handle would double-free.
244+
WGPUComputePipeline pipeline_q =
245+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
246+
WGPUComputePipeline pipeline_k =
247+
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
248+
249+
add_rope_dispatch(
250+
graph,
251+
device,
252+
pipeline_q,
253+
bgl,
254+
xq,
255+
xq_out,
256+
freqs_cos,
257+
freqs_sin,
258+
n_heads_q,
259+
seq,
260+
head_dim,
261+
xq_wgc);
262+
add_rope_dispatch(
263+
graph,
264+
device,
265+
pipeline_k,
266+
bgl,
267+
xk,
268+
xk_out,
269+
freqs_cos,
270+
freqs_sin,
271+
n_heads_k,
272+
seq,
273+
head_dim,
274+
xk_wgc);
275+
276+
wgpuShaderModuleRelease(shader);
277+
wgpuBindGroupLayoutRelease(bgl);
278+
wgpuPipelineLayoutRelease(pipeline_layout);
279+
// pipeline_q/pipeline_k owned by their dispatches; graph dtor frees.
280+
}
281+
282+
} // namespace
283+
284+
WEBGPU_REGISTER_OPERATORS {
285+
WEBGPU_REGISTER_OP(et_vk.apply_rotary_emb.default, apply_rotary_emb_impl);
286+
}
287+
288+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)